How would I go about fixating the viewport on a D3 (v4) map, such that users cannot zoom and pan outside of this viewport? Say I wanted to fix the viewport on South East Asia, and that my starting point is this fiddle. So pseudo code would be something like:
...constrainViewport([[x0,y0],[x1,y1]])
where x0,y0,x1 and y1 are the coordinates of the bounding viewport rectangle.
I have seen the following block from Sean Connelley however the solution presented is very complicated and out of date (v3). Happy to say I don't understand what is going on there either. Is there really no better (or 'native') way to achieve this result?
You can do this with zoom.translateExtent([a,b]) where a is a coordinate representing the top right and b is a coordinate representing the bottom left of the panning extent (it will constrain zoom when zooming out too).
Since you are working with geographic data, you can set your limits fairly easily in degrees of latitude and longitude:
// extent bounds (arbitrarily chosen)
var topLeft = projection([-180,80]);
var bottomRight = projection([0,-20]);
zoom.translateExtent([topLeft,bottomRight])
Here's an updated fiddle.
Related
I have played around with the d3js (v5) maps,
i'm trying to generate this map (the screenshot was taken from a random website),
For my particular case there is no need to present Antarctica.
I have read the documentation here: https://github.com/d3/d3-geo#projections,
and followed the instructions and used geoMercator, got this flat map which gets cutoff in the top north for some reason.
What is the correct approach for getting the first map's layout?
any suggestions?
The projection you are looking at is a Mercator projection.
With d3.geoMercator(), the scale value is derived from the circumference of the cylinder that forms the projection surface. The scale value is the number of pixels per radian. The default value anticipates stretching the 360 degrees of the cylinder over 960 pixels: 960/Math.PI/2.
For vertical angular distances, there is no such scaling factor, as one moves to extreme longitudes, the angular distance between points is increasingly exaggerated, such that the poles will be at ± infinity on the y axis. Because of this Mercator's, especially web Mercator's are often truncated at ±~85 degrees. With an extent of [-180,85] and [180,-85], a Mercator is square.
This limit is incorporated into d3-geoMercator, which "Defines a default projection.clipExtent such that the world is projected to a square, clipped to approximately ±85° latitude. (docs)"
This means that if we want to show the full extent of a d3-geoMercator, across 960 x 960 pixels, we can use:
d3.geoMercator()
.scale(960/Math.PI/2) // 960 pixels over 2 π radians
.translate([480,480]) // the center of the SVG/canvas
Which gives us:
The default center of d3-geoMercator is [0°,0°], so if we want [0°,0°] to be in the middle of the SVG/canvas, we translate the center so that it is in the middle, with a translate of [width/2,height/2]
Now that we are showing the whole world, we can refine to show only the portion we want. The simplest method might just be lopping off pixels from the bottom of the svg/canvas. Using the above code with a canvas/svg height of 700 pixels (and keeping 960 pixels across, using the same scale and translate) I get:
I did not remove Antarctica from this image - it just happens that it is cut off without having to filter it out (this is not necessarily ideal practice: it is still drawn).
So, an SVG/Canvas with width 960, height 700, with a projection scale of 960/Math.PI/2 and a translate of [480,480] appears to be ok. These values will scale together for different view port sizes.
With maps, there is often a lot of eyeballing to get the visual effect desired, tweaking projection.translate() or projection.center() can help shift the map to the desired location. But we can do this computationally. I'll speak to one method here, using projection.fitSize() (though this won't solve the required aspect ratio without extra steps).
Project.fitSize([width,height],geojson) takes an array specifying the dimensions of the SVG/canvas and a geojson object and tweaks the projection scale and translate values so that the geojson feature is contained in the SVG/canvas. The geojson feature could be a bounding box of the part of the world you want to show, so you could use:
projection.fitSize([width,height], {
type: "Polygon",
coordinates: [[
[-179.999,84] ,
[-179.999,-57] ,
[179.999,-57] ,
[179.999,84],
[-179.999,84]
]]
})
Where ~84 degrees north is the north end of Greenland and ~56 degrees south is roughly the tip of South America. This will ensure that the entire portion of the world you want to see is visible. However, as noted above, this doesn't consider aspect, so if you constrain the above extent to square dimensions, you'll still be showing the full extent of the Mercator.
In Mapbox, the fitBounds method doesn't seem to play nicely with points that are on both sides of the Atlantic (where the coords jump from 359 to 0).
How is it possible fit the bounds of Alaska or a flight from HK to SF. It might be possible to code a workaround making use of a center point and a custom zoom-level but it feel like a overengineered workaround around a poor handling of the bounds.
If firBounds is no help, is there another method like that would achieve similar results?
Example of fitBounds around Alaska, since Alaska crosses the antimeridian:
map.fitBounds([[172.461667,71.365162],[-129.979511,51.214183]])
Those cases are always a bit of a pain. What may help is wrapping your bounding box coordinates using:
https://www.mapbox.com/mapbox-gl-js/api/#lnglat#wrap
Another solution might be to convert the bounds to a center & zoom value using:
https://github.com/mapbox/geo-viewport
const {width, height} = map.getCanvas().getBoundingClientRect();
const viewport = geoViewport(
[minX, minY, maxX, maxY], // the bounding box (west, south, east north)
[width, height], // map dimensions in pixel
0, // minzoom
22, // maxzoom
512 // tilesize for mapbox-gl-js
);
map.setCenter(viewport.center);
map.setZoom(viewport.zoom)
I am using leaflet.js to create few markers and circles. I am using the below given code to draw circles : -
L.circle([ lat, lng ], 1000, {
color : colorCode,
stroke : false,
fillColor : colorCode,
fillOpacity : 0.7
});
Now if I edit this circle on UI and drag this circle vertically downwards, the circle size increases and vice a versa. Similar issue is with calling the above given method with different lat lngs. The same radius (1000) sized circle get plotted with different sizes on map.
My requirement is to place marker with same radius with same size on map everywhere.
I checked L.circleMarker but it takes radius in pixels and also circleMarkers does not scale in zoomin zoomout events. That is why I can't use circleMarkers.
I changed the crs option to 4326 but no success. I am using imageOverlay not tileset. I have created a fiddle.
http://jsfiddle.net/newBee_/88bdrzkr/12/
Try creating a circle on top area then edit and move it downwards. It's size increases. This is what I want to stop. This will resolve the problem of generating circle of same radius via code in different area of map with same size. Please help.
Please suggest.
Edit:
It looks like this is a bug deep into Leaflet 0.x: L.Circle radius computation uses hard-coded Earth projection rather than the specified CRS. Leaflet 1.0 seems to correctly check for the CRS before using the Earth-related computation.
For your case, simply overriding the faulty method seems to fix it, at least visually.
Demo: http://jsfiddle.net/88bdrzkr/13/
The "corrected" method to include in your script:
L.Circle.include({
_getLngRadius: function () {
return this._getLatRadius();
}
});
Regarding iH8's answer, the trick to override L.CRS.Simple.scale is similar to highly zooming (the 256 factor expands the latLng to much further pixels - any high number will do). At high zoom, you are moving your circle along a very short distance, for which the latitude does not change much. So you do not see any visible difference in radius, even though the bug is still there.
Demo of using just higher zoom, no method override at all: http://jsfiddle.net/kau6g8fk/1/
For your need where the circle looks to be more like a visual aid, any of these 3 solutions is enough.
Edit: the CRS is not the issue at all.
Previous message:
If you use Leaflet for indoor mapping, as your jsFiddle suggests (or any flat type map, as opposed to the projection of a sphere like Earth on to a plane), you could simply use L.CRS.Simple
Striked out this faulty solution as pointed out by Ghybs in his answer
Very weird issue, turns out that overloading L.CRS.Simple's scale method to return 256 * Math.pow(2, zoom) fixes this. Here's a fork of your JSFiddle: http://jsfiddle.net/kau6g8fk/ I'm unsure as to the cause of this issue, it would require more research. Will do if i find the time. Found the solution here: http://codepen.io/mike_beweb/pen/BymKGe
The answer below was given before the poster edited his/her question and showed that the used CRS was L.CRS.Simple while i presumed the default CRS. I'll leave it in tact because it might come in handy for some users:
The size change on drag of your L.Circle's is because of your map's default spherical mercator projection (EPSG:3857). Best explained with an image, here's a map with a graticule overlay on every 10 degrees:
Demo on Plunker: Leaflet 0.7.5 EPSG:3857 Spherical
As you move further from the equator every plane becomes higher. Thus your circle automaticly becomes higher the further north/south you drag it. You could use a equirectangular projection (EPSG:4326), in which every plane has the same size regardless of the distance from the equator:
Demo on Plunker: Leaflet 0.7.5 EPSG:4326 Equirectangular
With equirectangle projection you won't have the problem you're having now but you'll have to change your tileset to one with EPSG:4326 projection and those are hard to come by compared to EPSG:3857 tilesets.
If you're not willing or unable to change projection another solution could be to hack around L.CircleMarker and change the radius of your markers depended on current zoomlevel. But that's rather ugly in my opinion.
I'm trying to figure out how I can get the correct "active" tile under the mouse when I have "ramp" and +1 height tiles (see picture below).
When my world is flat, everything works no problem. Once I add a tile with a height of say +1, along with a ramp going back to +0, my screen -> map routine is still looking as if everything is "flat".
In the picture above, the green "ramp" is the real tile I want to render and calculate mouse -> map, however the blue tile you see "below" it is the area which gets calculated. So if you move your mouse into any of the dark green areas, it thinks you're on another tile.
Here is my map render (very simple)
canvas.width = canvas.width; // cheap clear in firefox 3.6, does not work in other browsers
for(i=0;i<map_y;i++){
for(j=0;j<map_x;j++){
var xpos = (i-j)*tile_h + current_x;
var ypos = (i+j)*tile_h/2+ current_y;
context.beginPath();
context.moveTo(xpos, ypos+(tile_h/2));
context.lineTo(xpos+(tile_w/2), ypos);
context.lineTo(xpos+(tile_w), ypos+(tile_h/2));
context.lineTo(xpos+(tile_w/2), ypos+(tile_h));
context.fill();
}
}
And here is my mouse -> map routine:
ymouse=( (2*(ev.pageY-canvas.offsetTop-current_y)-ev.pageX+canvas.offsetLeft+current_x)/2 );
xmouse=( ev.pageX+ymouse-current_x-(tile_w/2)-canvas.offsetLeft );
ymouse=Math.round(ymouse/tile_h);
xmouse=Math.round(xmouse/(tile_w/2));
current_tile=[xmouse,ymouse];
I have a feeling I'll have to start over and implement a world based map system rather than a simple screen -> map routine.
Thanks.
Your assumption is correct. In order to "pick" against world geometry, your routine needs to be aware of the world (and not just the base-level tile configuration). That is, without any concept of the height of the tiles near the one that is currently picked (by your current algorithm), there's no way to determine whether a neighboring tile (or one even further away, depending on the permitted height) should be intercepted by picking ray.
You've got the final possible point of your picking ray, already. What remains is to define the remainder of the ray, in world-space, and to check that ray for intersections with world geometry.
If, like the picture, your view angle is always 45 degrees and always from the same direction, your mouse -> map routine could use an algorithm something like:
calculate i,j of tile as you're doing currently (your final value of xmouse, ymouse)
look up height and angle of tile at i,j
given the height and angle, does this tile intersect the picking ray? If so, set lasti, lastj = i, j
increment/decrement i,j one step diagonally toward viewer
have we fallen off the edge of the map? If so, return lasti, lastj. Otherwise go back to 2.
Depending on the maximum height of a tile, you might have to check only 2 tiles, rather than going all the way to the edge of the map.
3 is the tricky part, and depends on your world geometry. Draw some triangles and you should be able to figure it out. Or you might try looking at the function intersect_quadrilateral_ray() here.
I'm using svg-pan-zoom library and I need to pan/zoom the view to fit a particular element.
I could use fit method but it fits the whole content in this case I need to fit only one particular element.
Another option can be to calculate the pan and zoom required and use the custom control, but how to get the pan/zoom of an element to fit the window?
UPDATE
I tried to follow the #bumbu "easier" solution. That was my first thought but I have encountered some troubled with the zooming point position.
This is a fiddle to show the expected behaviour and the calculation attempt.
http://jsfiddle.net/mgv5fuyw/2/
this is the calculation:
var bb=$("#target")[0].getBBox();
var x=bb.x+bb.width/2;
var y=bb.y+bb.height/2;
But somehow the zooming center expected (225,225) is not the right one.
I found a solution panning before zooming, I could not find the right way to use zoomAtPoint() method.
http://jsfiddle.net/mgv5fuyw/3/
var bb=$("#target")[0].getBBox();
var vbb=panZoomInstance.getSizes().viewBox;
var x=vbb.width/2-bb.x-bb.width/2;
var y=vbb.height/2-bb.y-bb.height/2;
var rz=panZoomInstance.getSizes().realZoom;
var zoom=vbb.width/bb.width;
panZoomInstance.panBy({x:x*rz,y:y*rz});
panZoomInstance.zoom(zoom);
Without going into detail I'd try 2 approaches:
Easier:
Init the svg-pan-zoom library
Fit and center you SVG
Calculate positions (top-left and bottom-right, or center and size) of the elements you're interested in
Now based on viewport size you should be able to calculate zoom level and center point of each element
Harder:
Figure out relative position of the original objects relative to original viewport
Based on current viewport size you should be able to calculate zoom level and center point of each element