Leaflet: Are custom zoom levels possible? - javascript

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

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>

OpenLayers draw LineString with real width in meters

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)

Leaflet: misalignment of tiles and negative y coordinates

I am trying to generate a custom non-geographical map with Leaflet using my own tiles.
For the moment I created 10x10 dummy tiles of size 256x256px each with a random solid color using imagemagick.
They are placed in public/map-tiles/1/map_x_y.png where x takes values from 0 to 9 (resp. y).
Since this is a non-geographical map, I paid attention to change crs to L.CRS.Simple (see http://leafletjs.com/examples/crs-simple/crs-simple.html):
var map = L.map('map', {
minZoom: 1,
maxZoom: 1,
center: [0, 0],
crs: L.CRS.Simple
}).setView([0, 0], 1);
L.tileLayer('/map-tiles/{z}/map_{x}_{y}.png', {
bounds: [[0, 0], [2560, 2560]],
tms: true
}).addTo(map);
However this produces tiles slightly shifted and thus misaligned:
Also, tiles with negative y coordinates are fetched, which results in 404d requests, as seen in the console.
What can be the cause of this behavior?
EDIT 1: as IvanSanchez pointed out, the misalignment was caused by the missing leaflet.css stylesheet.
I still have the problem with negative coordinates. As suggested, I added bounds (see updated code above). Observations:
with bounds [[0, 0], [2560, 2560]]: no tiles displayed altogether, blank screen.
with bounds [[-1280, -1280], [1280, 1280]]: all tiles displayed, but negative tiles fetched (eg (5,-1)) resulting in 404s.
EDIT 2: after several tests it looks like the negative tiles are indeed the product of the y-axis handling (http://leafletjs.com/examples/wms/wms.html). The origin is top left, y going downward. I expected the tiles below the origin to be fetched, not above.
What I tried in order to keep my convention with x and y both increasing (that is x increases to the right, y increases downward, tiles with positive coordinate components are fetched from 0 to 9):
setting tms: true for tileLayer. No success, tiles with negative y are still fetched .
changing {y} to {-y} in the tileLayer source path. Results in Error: No value provided for variable {-y}. My script is using Leaflet 1.3.1.
In a map with L.CRS.Simple, all TileLayers have infinite bounds by default.
If you want a TileLayer to request tiles only in a given area, read the Leaflet API documentation, specifically a TileLayer option named bounds (inherited from GridLayer options). Let me quote:
bounds
type LatLngBounds
default undefined
If set, tiles will only be loaded inside the set LatLngBounds.
You also mention:
Weirdly enough, it tries to fetch tiles with negative coordinates [...]
That's not weird, it's behaviour as designed. There is nothing inherently wrong (nor weird) with negative coordinates, and negative tile coordinates are valid and documented in some tile standards
So if you have 10x10 tiles of 256px in size ranging from [0, 0] to [10, 10], you might want something like
L.tileLayer('/map-tiles/map_{x}_{y}.png', {
bounds: [[0, 0], [2560, 2560]]
}).addTo(map);
If the center of your data is the [0, 0] point and your tiles span from [-5, -5] to [5, 5] you might instead want something like
L.tileLayer('/map-tiles/map_{x}_{y}.png', {
bounds: [[-1280, -1280], [1280, 1280]]
}).addTo(map);
The problem boils down to two aspects:
misalignment: the leaflet.css stylesheet was missing and simply needed to be linked to in the HTML of the page.
negative tiles fetched: for Leaflet the y-axis goes downward. I expected tiles to be fetched from left to right, top to bottom with an origin set to the top left corner. Instead, negative y's were fetched. Since my tiles' names are map_x_y.png where x and y take values in {0:9}, this resulted in 404d requests. Setting negative bounds fixed the issue with bounds: [[0,0],[-1230,1230]] (notice the minus sign). 1230 corresponds to the number of tiles at zoom 0 times the size in pixel of one tile.

Exclude overlaid element from Google Maps viewport bounds

I am using Google Maps API v3 to create an inline map on a website. In its container element, I also have an absolute positioned overlay which shows some detail information, visually hovering over the map. Determining on context this element may grow up to the size of the entire map element.
All this is working fine, however the Maps instance of course still considers the overlaid part of the map a valid usable part of the map. This means that, especially if the overlay is at maximum height, setCenter doesn't focus on the visible center, and routes drawn with DirectionsRenderer are partially underneath the overlay.
See this image:
Is there a way to limit the actual viewport to the blueish area, so that setCenter centers on the arrow tip and setBounds fits to the blue part?
I have managed to implement an acceptably functional workaround for the time being.
Some general notes which are good to know:
Every Map object has a Projection, which can convert between LatLng points to map points.
The map points a Projection uses for calculation are in 'world' coordinates, meaning they are pixels on the world map at zoom level 0.
Every zoom level exactly doubles the number of pixels shown. This means that the number of pixels in a given map point equals 2 ^ zoom.
The samples below assume a 300px wide sidebar on the right - adapting to other borders should be easy.
Centering
Using this knowledge, it becomes trivial to write a custom function for off-center centering:
function setCenter(latlng)
{
var z = Math.pow(2, map.getZoom());
var pnt = map.getProjection().fromLatLngToPoint(latlng);
map.setCenter(map.getProjection().fromPointToLatLng(
new google.maps.Point(pnt.x + 150/z, pnt.y)));
}
The crucial bits here are the z variable, and the pnt.x + 150/z calculation in the final line. Because of the above assumptions, this moves the point to center on 150 pixels to the left for the current zoom level, and as such compensates for the missing 300 pixels on the right sidebar.
Bounding
The bounds issue is far less trivial. The reason for this is that to offset the points correctly, you need to know the zoom level. For recentering this doesn't change, but for fitting to previously unknown bounds it nearly always will. Since Google Maps uses unknown margins itself internally when fitting to bounds, there is no reliable way to predict the required zoom level.
Thus a possible solution is to invoke a two-step rocket. First off, call fitBounds with the entire map. This should make the bounds and zoom level at least nearly correct. Then right after that, do a second call to fitBounds corrected for the sidebar.
The following sample implementation should be called with a LatLngBounds object as parameter, or no parameters to default to the current bounds.
function setBounds(bnd, cb)
{
var prj = map.getProjection();
if(!bnd) bnd = map.getBounds();
var ne = prj.fromLatLngToPoint(bnd.getNorthEast()),
sw = prj.fromLatLngToPoint(bnd.getSouthWest());
if(cb) ne.x += (300 / Math.pow(2, map.getZoom()));
else google.maps.event.addListenerOnce(map,'bounds_changed',
function(){setBounds(bnd,1)});
map.fitBounds(new google.maps.LatLngBounds(
prj.fromPointToLatLng(sw), prj.fromPointToLatLng(ne)));
}
What we do here at first is get the actual points of the bounds, and since cb isn't set we install a once-only event on bounds_changed, which is then fired after the fitBounds is completed. This means that the function is automatically called a second time, after the zoom has been corrected. The second invocation, with cb=1, then offsets the box to correct for the 300 pixel wide sidebar.
In certain cases, this can lead to a slight off-animation, but in practice I've only seen this occur when really spamclicking on buttons causing a fit operation. It's running perfectly well otherwise.
Hope this helps someone :)
You can use the map panBy() method which allows you to change the center of the map by a given distance in pixels.
Hope this helps!
I had a similar need and ended up just forcing some "padding" to the east of a LatLngBounds object.
On the upside, it's simple and it works. On the downside it's not really versatile. Just a quick little hack.
// start with a standard LatLngBounds object, extending as you usually would...
bounds = new google.maps.LatLngBounds();
// ...
ne = bounds.getNorthEast();
sw = bounds.getSouthWest();
// the multiplier used to add space; positive for east, negative for west
lngPadding = 1.5
extendedLng = ne.lng() + (ne.lng() - sw.lng()) * lngPadding;
// copy original and extend with the new Lng
extendedBounds = bounds;
extendedBounds.extend(new google.maps.LatLng(ne.lat(), extendedLng));
map.fitBounds(extendedBounds);

Using ClusterProvider in Nokia/Here Maps Javascript API, invalid cluster numbers

I'm trying to add the cluster to my application and so far, everything works.
However, the number of items in clusters seems to be invalid depending on zoom levels.
For example, I add 3 marker in a range of about 30 feets. If I'm zoomed in all the way, I see all 3 markers. If I zoom out just a few steps, I can see 2 markers plus a cluster indicating 3 items.
I attached a picture, top part of the pictures shows the problem. If I zoom in a bit, it shows the bottom part of the picture. If I zoom out more, it shows a cluster of 3.
Thanks
Try fiddling around with the ClusterProvider.Options. Obviously all clustering algorithms are an approximation of the actual data set, and maybe the particular distribution of points you have just doesn't look good at a high zoom using the defaults.
Here are three suggestions to try:
Lower the eps value to get a finer grid.
Set max and min or minPts to avoid clustering at lower levels.
Set the strategy to STRATEGY_GRID_BASED rather than use the density default.
e.g. something like this:
function clusterDataPoints(data){
clusterProvider = new nokia.maps.clustering.ClusterProvider(map, {
eps: 5,
minPts: 5,
min: 18,
strategy: nokia.maps.clustering.ClusterProvider.STRATEGY_GRID_BASED,
dataPoints: data
});
clusterProvider.cluster();
}
And keep altering the parameters until it "looks right"

Categories

Resources