OpenLayers draw LineString with real width in meters - javascript

I tried to draw a polyLine (road lane) with width in meters using the latest stable OpenLayers library, but it didn't have the correct real width (checked on top of Google Maps layer).
This is the code snippet that works for me:
polyLineFeature.setStyle((feature, resolution) => {
const lineString = feature.getGeometry();
const lineStyles = [
new Style({
stroke: new Stroke({
width:
laneWidth /
resolution /
getPointResolution("EPSG:3857", 1, lineString.getCoordinateAt(0.5)),
lineCap: "square",
}),
}),
];
return lineStyles;
});
I use the default projection everywhere (web mercator).
But I don't get the reason why do I have to divide the real width in meters by the current resolution AND another resolution that is relevant within the actual area.
Is this explained anywhere?

WebMercator 3857 is a bad projection with respect to distances. It introduces a huge distortion as you move away from the equator (that's why Greenland looks so big on the map while in reality it is a fairly small piece of land).
By dividing the width by getPointResolution, you are effectively correcting this distortion.
If you were using another projection that preserve distances, such as UTM, you wouldn't have to do this division (or, if you did, the value of getPointResolution would be 1)

Related

How can I prevent Leaflet from splitting on what appears to be 180 longitude? [duplicate]

Is there a way to get a shape to wrap from one edge across the dateline meridian (180° longitude) to appear on the other side of the map in Leaflet.js?
I've looked at:
http://leafletjs.com/reference.html#latlng-wrap
https://github.com/Leaflet/Leaflet/issues/82
But I'm unsure on what I could do to get it to reliably draw across the dateline...
Thank you in advance!
Oh, you're hitting antimeridian artifacts. You're not the first one, and will not be the last one.
In Leaflet, there are basically two approaches for this problem:
1a: Cut the polygon beforehand
If you know your GIS tools, preprocess your polygon, so you end up with two (or possibly more) polygons. See «How can I make a polyline wrap around the world?».
Once you have a file with several polygons which don't cross the antimeridian, they should render fine. You will hit artifacts (namely, a vertical polygon border at the antimeridian, spanning the inside ofthe polygon) if you apply a border to the polygons, so you might want to cut a polygon and a polyline with the polygon's edge if you want to render both nicely.
1b: Cut the polygon on the browser
If you don't want to cut the polygon beforehand, you can let the web browser do it on the fly.
There are some utilities that can help here, but I'm going to point to Leaflet.VectorGrid in particular. By leveraging geojson-vt, it can cut polygons and their edges into tile-sized polygons and polygon edges. It can handle geometries crossing the antimeridian quite well.
You might want to look into geojson-vt directly, or maybe turf.js to do some on-the-fly geoprocessing.
2: Think outside the [-180..180] range
Leaflet can handle longitudes outside the [-180..180] range. In Leaflet, longitudes wrap only the TileLayer's tiles and not markers or polylines.
In other words: a marker at [0, -179] is shown at a different place than [0, 181]. See this answer for an example.
In other words: a line from [0, 179] to [0, -179] is 358 degrees long, but a line from [0, 179] to [0, 181] is two degrees long.
In other words: you can have linestrings or polygons with coordinates with longitudes outside the [-180..180] range, and that's fine for Leaflet. It's not fine for a lot of GIS software (in fact, I think that the new GeoJSON spec prohibits it). But it will make Leaflet happy.
When you are working with a cylindrical projection, as Leaflet does, it can be solved relatively easily with trigonometry. My solution is based on the first approach of Ivan's answer above, which is cutting the line in two parts at the 180th meridian. My solution is not perfect, as I will show below, but it is a good start.
Here is the code:
function addLineToMap(start, end) {
if (Math.abs(start[1] - end[1]) > 180.0) {
const start_dist_to_antimeridian = start[1] > 0 ? 180 - start[1] : 180 + start[1];
const end_dist_to_antimeridian = end[1] > 0 ? 180 - end[1] : 180 + end[1];
const lat_difference = Math.abs(start[0] - end[0]);
const alpha_angle = Math.atan(lat_difference / (start_dist_to_antimeridian + end_dist_to_antimeridian)) * (180 / Math.PI) * (start[1] > 0 ? 1 : -1);
const lat_diff_at_antimeridian = Math.tan(alpha_angle * Math.PI / 180) * start_dist_to_antimeridian;
const intersection_lat = start[0] + lat_diff_at_antimeridian;
const first_line_end = [intersection_lat, start[1] > 0 ? 180 : -180];
const second_line_start = [intersection_lat, end[1] > 0 ? 180 : -180];
L.polyline([start, first_line_end]).addTo(map);
L.polyline([second_line_start, end]).addTo(map);
} else {
L.polyline([start, end]).addTo(map);
}
}
This will calculate the latitude where the line crosses the 180th meridian, and draw the first line from the starting point to this latitude on the 180th meridian, and then a second one from this point to the end.
The picture below shows an example of the result.
Even though I'm fairly certain the math checks out on my calculations, there is a small kink where the two lines are separated. I'm not sure whether this is due to the rendering of the Leaflet map, or an actual error in my calculations.
The starting point is [35.552299, 139.779999] and the end point is [64.81510162, -147.8560028].
The total longitudinal difference between the points is 72.364, and latitudinal difference is 29.263. Using the code below or an online calculator, the angle α is 22.018. Taking only the distance from the starting point to the 180th meridian, and the angle α, the latitudinal difference between starting point and intersection is 16.264. Adding the latitude of the starting point and this value, we get a latitude of 51.8166 at the 180th meridian. Drawing a straight line on a map tells me that this value should be slightly higher up, but I can't figure out why or how that is calculated.
If you want a curved line that accurately shows the curvate of the earth, I would highly recommend using Leaflet.Geodisic. It is easy to use and has a solution to the antimeridian problem built-in so you don't have to worry about it.
If you're using react-leaflet, the easiest way is to use it together with leaflet.geodesic and set the lines with leaflet.geodesic.
Import the required libraries
import { GeodesicLine } from "leaflet.geodesic"
import * as L from "leaflet"
import {
MapContainer,
} from "react-leaflet"
Some points to join up across the meridian
const East = new L.LatLng(-41.75412, 175.70595)
const FurthurEast = new L.LatLng(-42.43624, -178.65339)
const Chile = new L.LatLng(-26.74165, -71.41818)
In the return of your component:
<MapContainer
center={centerMarker as [number, number]}
zoom={5}
whenCreated={(mapInstance) => {
new GeodesicLine([East, FurthurEast, Chile], {
weight: 10,
color: "red",
}).addTo(mapInstance)
}}
>
{...children}
</MapContainer>

d3 fitSize doesn't fit the extent as expected using a geojson polygon

I'm using D3 to draw geo data on top of a static map.
Before drawing the data, I'm using d3's fitSize method to set the projection so that the bounds of the map (as spherical lat lng pairs) fit the full extent of the canvas.
const size = {
width: 1048,
height: 447
};
const bounds = [
-13.098400070906848,
51.44391918744,
17.18913007090731,
42.66553377959798
];
const geojson = turf.bboxPolygon([bounds[0], bounds[1], bounds[2], bounds[3]])
const projection = d3.geoMercator().fitSize([size.width, size.height], geojson);
d3.geoPath().projection(projection);
In this example, the projection doesn't fit the extent, the scale is off by a large margin.
I should note that I get the bounds from the map beforehand so the issue shouldn't be due to a proportions difference between the canvas size and the bounds.
I've put together an example here where the projection doesn't fit. It shows where the projection draws the bounds compared to the extent.
As you can see in the logs, the ratios of the bounds dimensions and the canvas dimensions are almost identical, meaning that we should be able to scale the bounds to the full extent of the canvas.
I'm confused as to why that is and how to fix it.
Any help would be greatly appreciated! Thanks!

Diameter or drawing way off when zoom is >13 on Google Maps

I'm writing some drawing tools for Google Maps where a user selects a tool and clicks and drags to get a distance. Here's a gif of what the "ruler" tool looks like:
I made a rectangle one too and that works perfect as well. I'm having issues though with a Circle tool in calculating the diameter or radius of the circle once the zoom level is greater than 13. You can see the distance in the gifs below. The first one is zoom level 13, next is 14.
Here's the code I have:
var diameter = drawingManager.distanceBetweenTwoLatLng(
this._startPosition,
drawingManager.fromEventToLatLng(event)
);
this.circle.setOptions({
// After level 14 zoom we don't multiply *1000. *1000 is a Magic Number™
// and I have no idea why I need it or why zoom level 14 needs to not
// have it but 13 and does.
radius: map.getZoom() > 13 ? diameter : diameter * 1000
});
I calculate the pixels to LatLng with this (and where I think it might be failing because it gets the scale?):
var map = this.settings.map;
var projection = map.getProjection();
var topRight = projection.fromLatLngToPoint(map.getBounds().getNorthEast());
var bottomLeft = projection.fromLatLngToPoint(map.getBounds().getSouthWest());
var scale = 1 << map.getZoom();
return projection.fromPointToLatLng(new google.maps.Point(x / scale + bottomLeft.x, y / scale + topRight.y));
The problem is the drawingManager methods shown above (distanceBetweenTwoLatLng and fromEventToLatLng) work totally fine with all the other tools zoomed at any level.
You can see my current workaround is just checking for the zoom level and giving it different radius settings.
After more debugging my coworker pointed out that maybe the distance scale was off. I had assumed (incorrectly) that the distance scale needed to be what google maps is set to. So if the user has it on miles it would be miles, for example. I went to their docs and saw it needed to be in meters. Seems so obvious now :\ Anyway, my issue was that i was using the user's current distance scale rather than m which is what Google's Circle shape uses exclusively.

Leaflet: Are custom zoom levels possible?

Is it possible to have intermediate (2.5, 3.5, 4.5, etc.) zoom levels on a Leaflet map that is using Stamen Toner-lite tiles? This is the code I have so far that calculates the zoom level:
leafletmap.on('zoomstart', function (d){
targetZoom = leafletmap.getZoom(); //Grabs whatever current zoom level is
targetZoom = targetZoom +.5; //Adds .5
leafletmap.setZoom(targetZoom); //Sets new value to zoom level
console.log(targetZoom); //Consoles out new value
});
I tried just adding .5 to the code, but I get a too much recursion error, so I'm guessing it's not that simple. Any help or direction is greatly appreciated!
In version 1.0.0, Leaflet introduced fractional zooming:
https://leafletjs.com/examples/zoom-levels/#fractional-zoom
Before this, the zoom level of the map could be only an integer number
(0, 1, 2, and so on); but now you can use fractional numbers like 1.5
or 1.25.
...
If you set the value of zoomSnap to 0.5, the valid zoom levels of the
map will be 0, 0.5, 1, 1.5, 2, and so on.
If you set a value of 0.1, the valid zoom levels of the map will be 0,
0.1, 0.2, 0.3, 0.4, and so on.
The following example uses a zoomSnap value of 0.25:
var map = L.map('map', {
zoomSnap: 0.25
});
As you can see, Leaflet will only load the tiles for zoom levels 0 or
1, and will scale them as needed.
Leaflet will snap the zoom level to the closest valid one. For
example, if you have zoomSnap: 0.25 and you try to do
map.setZoom(0.8), the zoom will snap back to 0.75. The same happens
with map.fitBounds(bounds), or when ending a pinch-zoom gesture on a
touchscreen.
To be straight to the point: This is not possible. You would need to render your own tile-images, run them of your own server and create your own coordinate reference system (CRS) extension for Leaflet. If you look at how regular tilesets are made you'll understand why.
The URL for requesting tiles for stamen:
http://{s}.tile.stamen.com/toner/{z}/{x}/{y}.png
When requesting tiles, the {z} will be replaced with the map's current zoomlevel. The {x} and {y} are the coordinates for the tile. The {s} will be replaced with a subdomain. So if your at zoomlevel 6 at coordinate 1,1 it would try to load:
http://a.tile.stamen.com/toner/6/1/1.png
Now if you could (but you can't) zoom to level 6.5 it would try to load:
http://a.tile.stamen.com/toner/6.5/1/1.png
Those tiles simple don't exists on the stamen server and thus return a 404 for file not found. You can try for yourself just use these links:
http://a.tile.stamen.com/toner/6/1/1.png
http://a.tile.stamen.com/toner/6.5/1/1.png
http://a.tile.stamen.com/toner/7/1/1.png
So that will never work. You could, as said, run your own tile server, render your own tile images and setup your own L.CRS. You might want to take a look at this question too: Adding an extra zoom levels in Leaflet Maps

What does it mean to scale a projection in d3?

I know that in D3, scales are mathematical maps from input data values (domain) to output data values (range). I know that I can set up a scale that will map input from the domain onto a range, like this:
var scale = d3.scale.linear().domain([100, 500])
.range([10, 350]);
scale(100); //Returns 10
I know that once you setup a scale, you can use it to scale attributes, like this:
.attr("cx", function(d) {
return scale(d[0]); //Returns scaled value
})
However, on Bostock's mapping tutorial scale is used a little differently. Bostock calls scale on whatever is returned by the mercator projection:
var projection = d3.geo.mercator()
.scale(500)
.translate([width / 2, height / 2]);
I am trying to understand that line of code and having a little trouble. mercator() returns something -- which is not so clear from the API -- and then the scale method is called on that object with an input value of 500.
What does it mean to call "scale" on a projection like this? How does this relate to scale() as a way to transform 100 in 10 -- as in the example above.
More immediately, if I add this to my code, my geojson map disappears! How do I make it scale properly?
var projection = d3.geo.mercator()
.scale(500)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection); //if I add this little bit to the path, my map disappears!
Here is the GeoJSON I am using: http://geojson.io/#id=gist:AbeHandler/9d28239c592c6b552212&map=10/29.9912/-89.9320
In this case, scale is used in the general sense of the size of something. It's not directly related to the d3 scale functions, but is related to the scale transforms you can apply to an SVG element to make it bigger or smaller. However, instead of scaling a flat drawing, it scales the complex projection.
The projection returned by d3.geo.mercator() is a function. Specifically, it's a function that converts from a longitude/latitude point to an x/y point.
Similarly, the function returned by d3.geo.path() converts GeoJSON data into SVG path definitions. When you assign a modify this function by assigning it a specific projection function, it will use that projection to figure out the position of each point on the path it creates.
Now, why are you not seeing anything at all when you assign a scaled-up projection to your path? It is probably simply that you are so zoomed-in that there is nothing to see: you're lost in empty ocean. The default scale factor on a projection is 150, so a scale of 500 is more than three times zoomed in. Try different scale factors, some bigger and some smaller than 150, to zoom in and out of the map.
Of course, the translation could also be throwing off your map. The translation parameter sets the x/y value for the center (latitude/longitude) point of your map. If you don't specify what you want to use as a center point, the projection uses 0 degrees latitude and 0 degrees longitude. If the geography you're trying to draw is nowhere near that point (which is in the Gulf of Guinea, off the coast of Ghana), then once again nothing is going to show up unless you zoom out considerably (i.e., use a very small scale number).

Categories

Resources