Leaflet - Calculate view bounds in Km - leaflet

I'm trying to figure out how to calculate the distance in KM of the current view in Leaflet.
For now I'm retrieving data from the API using the getBounds() of the current view, which gives me the SW&NE coords. Is there a way to know the widht & height in KM of the current view?
Image example of what I'm trying to achieve.
The purpose of this is to use Redis to get coordinates using BYBOX feature, but it only accepts distances.
Thanks in advance.

You can calc the distance between the latlngs of the bounds:
function getWidthHeightInKM(){
var bounds = map.getBounds();
var width = map.distance(bounds.getNorthWest(), bounds.getNorthEast()) / 1000;
var height = map.distance(bounds.getNorthWest(), bounds.getSouthWest()) / 1000;
return {
width,
height
}
}
console.log(getWidthHeightInKM())
Demo: https://plnkr.co/edit/8cjjQiA9rWWeBWH3

Related

Leaflet meters to pixels per zoom level

I'm creating an app using Leaflet library
I have a field radius that contains distance in meters (m).
const radius = 1000;
I would need to convert this field to pixels, but on different zoom levels
For zoom levels, I get values from 8 till 18
I have this function that converts successfully on the current zoom level
function disToPixeldistance(distance){
var l2 = L.GeometryUtil.destination(map.getCenter(),90,distance);
var p1 = map.latLngToContainerPoint(map.getCenter())
var p2 = map.latLngToContainerPoint(l2)
return p1.distanceTo(p2)
}
But I would need to pass different zoom levels as an additional parameter and then convert them from meters to pixels
Like:
function disToPixeldistance(distance, zoomLevel)
Does anyone knows how could I achieve this ? Thank you in advance :)
Change the map.latlngToContainerPoint(latlng) to map.project(latlng,zoom)
function disToPixeldistance(distance, zoom){
zoom = zoom || map.getZoom();
var l2 = L.GeometryUtil.destination(map.getCenter(),90,distance);
var p1 = map.project(map.getCenter(), zoom)
var p2 = map.project(l2,zoom)
return p1.distanceTo(p2)
}

Manually write world file (jgw) from Leaflet.js map

I have the need to export georeferenced images from Leaflet.js on the client side. Exporting an image from Leaflet is not a problem as there are plenty of existing plugins for this, but I'd like to include a world file with the export so the resulting image can be read into GIS software. I have a working script fort his, but I can't seem to nail down the correct parameters for my world file such that the resulting georeferenced image is located exactly correctly.
Here's my current script
// map is a Leaflet map object
let bounds = map.getBounds(); // Leaflet LatLngBounds
let topLeft = bounds.getNorthWest();
let bottomRight = bounds.getSouthEast();
let width_deg = bottomRight.lng - topLeft.lng;
let height_deg = topLeft.lat - bottomRight.lat;
let width_px = $(map._container).width() // Width of the map in px
let height_px = $(map._container).height() // Height of the map in px
let scaleX = width_deg / width_px;
let scaleY = height_deg / height_px;
let jgwText = `${scaleX}
0
0
-${scaleY}
${topLeft.lng}
${topLeft.lat}`
This seems to work well at large scales (ie zoomed in to city-level or so), but at smaller scales there is some distortion along the y-axis. One thing I noticed is that all examples of world files I can find (and those produced from QGIS or ArcMap) all have the x-scale and y-scale parameters being exactly equal (oppositely signed). In my calculations, these terms are different unless you are sitting right on the equator.
Example world file produced from QGIS
0.08984380916303301 // x-scale (size of px in x direction)
0 // rotation parameter 1
0 // rotation parameter 2
-0.08984380916303301 // y-scale (size of px in y direction)
-130.8723208723141056 // x-coord of top left px
51.73651369984968085 // y-coord of top left px
Example world file produced from my calcs
0.021972656250000017
0
0
-0.015362443783773333
-130.91308593750003
51.781435604431195
Example of produced image using my calcs with correct state boundaries overlaid:
Does anyone have any idea what I'm doing wrong here?
Problem was solved by using EPSG:3857 for the worldfile, and ensuring the width and height of the map bounds was also measured in this coordinate system. I had tried using EPSG:3857 for the worldfile, but measured the width and height of the map bounds using Leaflet's L.map.distance() function. To solve the problem, I instead projected corner points of the map bounds to EPSG:3857 using L.CRS.EPSG3857.project(), the simply subtracted the X,Y values.
Corrected code is shown below, where map is a Leaflet map object (L.map)
// Get map bounds and corner points in 4326
let bounds = map.getBounds();
let topLeft = bounds.getNorthWest();
let bottomRight = bounds.getSouthEast();
let topRight = bounds.getNorthEast();
// get width and height in px of the map container
let width_px = $(map._container).width()
let height_px = $(map._container).height()
// project corner points to 3857
let topLeft_3857 = L.CRS.EPSG3857.project(topLeft)
let topRight_3857 = L.CRS.EPSG3857.project(topRight)
let bottomRight_3857 = L.CRS.EPSG3857.project(bottomRight)
// calculate width and height in meters using epsg:3857
let width_m = topRight_3857.x - topLeft_3857.x
let height_m = topRight_3857.y - bottomRight_3857.y
// calculate the scale in x and y directions in meters (this is the width and height of a single pixel in the output image)
let scaleX_m = width_m / width_px
let scaleY_m = height_m / height_px
// worldfiles need the CENTRE of the top left px, what we currently have is the TOPLEFT point of the px.
// Adjust by subtracting half a pixel width and height from the x,y
let topLeftCenterPxX = topLeft_3857.x - (scaleX / 2)
let topLeftCenterPxY = topLeft_3857.y - (scaleY / 2)
// format the text of the worldfile
let jgwText = `
${scaleX_m}
0
0
-${scaleY_m}
${topLeftCenterPxX}
${topLeftCenterPxY}
`
For anyone else with this problem, you'll know things are correct when your scale-x and scale-y values are exactly equal (but oppositely signed)!
Thanks #IvanSanchez for pointing me in the right direction :)

Calculate lat long on static mapbox img for 256px tiles

I have a static image with only center point lat/long (for example https://api.mapbox.com/styles/v1/mapbox/light-v9/static/-78.4649,42.5128,5,0,0/300x200) and I want to put on this map some markers(lat.long) with the help of canvas.
But I need to calculate somehow the xy coordinates for those markers.
So I know the center of map(lat/long) and the lat/long marker coordinates. Is there any way to convert lat/long to xy knowing only zoom level and center?
Or if I know the xy of the center lat/long(it always be the same 150px * 100px) and zoom level, could I calculate the xy for other markers?
I have a lot of markers (>200, and they all are custom svg generated and so on) to place it on this map. I can't use mapbox mapbox static map because of the markers limitation and so on.
UPD: Based on the comments I updated the question.
How to calculate it for 256px square tiles?
Based on the OP comment I'm assuming that the requested image is square, for the sake of simplicity (TILE_SIZE could be decomposed in a TILE_SIZE_X and TILE_SIZE_Y component). I'm also assuming that the image is 256-pixels wide TILE_SIZE=256
I'm giving both the pixel coordinates relative to the center of the image (distanceInPixels function), and to the Lower Left Corner (imageCoordinates function). Changing to the Upper Left Corner in case that's necessary should be trivial (X will be equal and Y = TILE_SIZE -Y).
<!DOCTYPE html>
<html>
<body>
<p id="demo"></p>
<script>
var latLngMarker = {};
var latLngCenter = {};
// Image dimensions in pixels
var TILE_SIZE = 256;
var zoom = 5;
// Coordinates of the marker to be projected on the image
latLngMarker.lat = 41.850;
latLngMarker.lng = -87.650;
// Coordinates of the image center
latLngCenter.lat = 41.850;
latLngCenter.lng = -87.650;
// Coordinates projected on the cartographic plane (Mercator)
var centerProjected = project(latLngCenter);
var markerProjected = project(latLngMarker);
// The result should be X=Y=0, because I made Marker Lat Lng = Center Lat Lng
var distanceFromCenter = distanceInPixels(centerProjected, markerProjected);
alert("X: " + distanceFromCenter.x + " Y: " + distanceFromCenter.y);
// The result should be X=Y=256/2=128 for the same reason
var coords = imageCoordinates(centerProjected, markerProjected);
alert("X: " + coords.x + " Y: " + coords.y);
// The horizontal distance represented by one pixel for a given latitude and zoom level
function pixelResolution (latLng, zoom) {
var radius = 6378137.0 // semi-axis of WGS84 ellipsoid
var circumference = 2 * Math.PI * radius;
var distancePerImage = circumference * Math.cos(latLng.lat * Math.PI / 180.0) / Math.pow(2,zoom);
var distancePerPixel = distancePerImage / TILE_SIZE;
return distancePerPixel
}
// Web mercator projection.
function project(latLng) {
var siny = Math.sin(latLng.lat * Math.PI / 180);
siny = Math.min(Math.max(siny, -0.9999), 0.9999);
var xy = {};
xy.x = TILE_SIZE * (0.5 + latLng.lng / 360);
xy.y = TILE_SIZE * (0.5 - Math.log((1 + siny) / (1 - siny)) / (4 * Math.PI));
return xy
}
// Marker pixel coordinates relative to the image Center
function distanceInPixels(centerProjected, markerProjected) {
var delta = {};
var spacing = pixelResolution(latLngCenter, zoom);
delta.x = Math.round((centerProjected.x - markerProjected.x)/spacing);
delta.y = Math.round((centerProjected.y - markerProjected.y)/spacing);
return delta
}
// Marker pixel coordinates relative to the Lower Left Corner
function imageCoordinates(centerProjected, markerProjected) {
var pixelCoordinates = {};
var spacing = pixelResolution(latLngCenter, zoom);
var deltaPixels = distanceInPixels(centerProjected, markerProjected);
pixelCoordinates.x = TILE_SIZE / 2 - deltaPixels.x;
pixelCoordinates.y = TILE_SIZE / 2 - deltaPixels.y;
return pixelCoordinates
}
</script>
</body>
</html>
Note: I can confirm that the pixelResolution function only works with square image tiles with dimensions of powers of 2. The Math.pow(2,zoom); snippet gives the game away!
Web Mercator function based on:
https://developers-dot-devsite-v2-prod.appspot.com/maps/documentation/javascript/examples/map-coordinates
Horizontal distance represented by one pixel from :
https://wiki.openstreetmap.org/wiki/Zoom_levels
See also:
https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Resolution_and_Scale
If you're going to linearly interpolate you'd need to know the lat/long & x/y for 2 points. It wouldn't be possible with only the center point unless you also have a conversion metric for pixels - ie. 50 pixels is .1 delta lat/long.
If you have the lat/long & x/y for two points you can create the ratio as y1 - y2 / lat1-lat2 or x1-x2/long1-long2 each of which should result in the same ratio
Then it'd be relatively easy, assume the ratio is 5 meaning 5px/l so you had a point that was (3,-4) away from that center point you'd simply multiple to find the pixel offset (15,-20) and add that to the center = (165, 80).
Since all of your images are zoomed the same amount you could manually calculate the ratio once and store it as a constant.
sudo/untested python:
def getRatio(latlongs=[(1,1),(0,0)], xys=[(5,5),(0,0)]:
return (xys[0][1]-xys[1][1]) / (latlongs[0][0] - latlongs[1][0])
centerLatLong = (5,5)
centerXY = (150, 100)
def getCoord(lat,long,ratio):
y = (lat-centerLatLong[0])*ratio + centerXY[1]
x = (long-centerLatLong[1])*ratio + centerXY[0]
return x, y

How to set correct image dimensions by LatLngBounds using ImageOverlay?

I want to use ImageOverlays as markers, because I want the images to scale with zoom. Markers icons always resize to keep their size the same when you zoom.
My problem is that I can't figure out how to transform pixels to cords, so my image isn't stretched.
For instance, I decided my south-west LatLng to be [50, 50]. My image dimensions are 24px/24px.
How do I calculate the north-east LatLng based on the image pixels?
You are probably looking for map conversion methods.
In particular, you could use:
latLngToContainerPoint: Given a geographical coordinate, returns the corresponding pixel coordinate relative to the map container.
containerPointToLatLng: Given a pixel coordinate relative to the map container, returns the corresponding geographical coordinate (for the current zoom level).
// 1) Convert LatLng into container pixel position.
var originPoint = map.latLngToContainerPoint(originLatLng);
// 2) Add the image pixel dimensions.
// Positive x to go right (East).
// Negative y to go up (North).
var nextCornerPoint = originPoint.add({x: 24, y: -24});
// 3) Convert back into LatLng.
var nextCornerLatLng = map.containerPointToLatLng(nextCornerPoint);
var imageOverlay = L.imageOverlay(
'path/to/image',
[originLatLng, nextCornerLatLng]
).addTo(map);
Demo: http://playground-leaflet.rhcloud.com/tehi/1/edit?html,output

Leaflet latLngToContainerPoint and containerPointToLatLng not reciprocal?

Anybody know why the following is not reciprocal? latLng and new
var point = dispmap.latLngToContainerPoint(latlng);
var newPoint = L.point([point.x, point.y]);
var newLatLng = dispmap.containerPointToLatLng(newPoint);
When I execute this code I send in latlng=(26.75529,-80.93581)
newLatLng, which by inspection of the code above I would expect to reciprocate gives back...
newLatLng = (26.75542,-80.93628)
I'm wanting to array some markers with identical lat-lons around the shared spot on a map, and bumping each by some screen coordinates looks like the best method based on some blog/issue reading I've done.
I'm, "close" to what I want to achieve, but as I try to validate what these leaflet calls are doing for me I hit the fundamental question above.
They can't be ...
Latitude and longitude are float values while x and y are integer values.
This means that there are an (theoretically) infinite number of latlng's and a rather small number of points on your view (width * height).
Furthermore, I'm not sure how you define identical latlng's; the best you can't to is to speak of proximity.
If I read between the lines, identical may mean that the markers overlap. Then the best way is to have a look how Leaflet.MarkerCluster are tackling with the problem.
I was able to achieve my desired result by altering zoom level to avoid pixel-point quantization effects on my translations. The screenshot below illustrates an orange and two green circle markers that represent an identical lat-lon, but I want the green arrayed around the orange in a circular fashion...in this example there are only 2 green.
I perform simple circular array math with an angular step size of PI/4 in this example. The KEY to getting the visual effect correct is the "dispmap.setZoom(dispmap._layersMaxZoom)" call BEFORE I do the math, and then I invoke "dispmap.setZoom(mats.zoom)" after the math, which will give the user the desired zoom level as specified by variable mats.zoom.
var arrayRad=20;
var dtheta=Math.PI/4;
var theta=0;
dispmap.setZoom(dispmap._layersMaxZoom)
L.geoJson(JSON.parse(mats.intendeds), {
pointToLayer: function (feature, latlng) {
var point = dispmap.latLngToContainerPoint(latlng);
dx = arrayRad*Math.cos(theta);
dy = arrayRad*Math.sin(theta);
theta += dtheta;
var newPoint = L.point([point.x + dx, point.y+ dy]);
var newLatLng = dispmap.containerPointToLatLng(newPoint);
return L.circleMarker(newLatLng, intendedDeliveryLocationMarkerOptions);
}, onEachFeature: onEachIntendedLocFeature }).addTo(dispmap);
dispmap.setZoom(mats.zoom);
Sample screen shot at max zoom level: 2 arrayed markers