I'm using the Google Maps API. When I click on a map marker, I want the map to zoom in one step, but maintain the marker's position relative to the window.
This is similar to when you use the mouse wheel to zoom: the zooming is towards the current cursor position.
The map.setZoom() function zooms relative to the center of the map. This is not what I want.
Can't find any simple solution to this, anyone has a clue?
Hy please try this. Automatically zooming marker.
float zoomLevel = 16.0; //This goes up to 21
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, zoomLevel));
This kind of works, not an elegant solution:
function zoomInAtMarker(marker)
{
var pos = marker.getPosition();
var bounds = map.getBounds();
map.setZoom(map.getZoom() + 1);
var span = map.getBounds().toSpan();
var s = pos.lat() - span.lat() * (pos.lat() - bounds.getSouthWest().lat()) / bounds.toSpan().lat();
var w = pos.lng() - span.lng() * (pos.lng() - bounds.getSouthWest().lng()) / bounds.toSpan().lng();
map.panToBounds({ south:s, north:s + span.lat(), west:w, east:w + span.lng()});
}
Related
EDITED:
I'm trying to work out when Google Maps API map.panTo(Lat, Lng) decides the trip is too far (pixels) for a "smooth" pan.
This is what the manual has to say: -
Changes the center of the map to the given LatLng. If the change is less than both the width and height of the map, the transition will be smoothly animated.
I've established that if there is only x or y vertex movement (only either Lat or Lng value changes but not both) then the check is a simple two-thirds .6666 of the map's viewport width or height. But if both Lat and Lng values change then I'm not sure of the formula.
An example of what we know: -
If we travel from Perth to somewhere up near Yeppoon: -
Perth: Lat: -31.9523 Lng: 115.8613 xPixel: 13465 yPixel: 9728
Yeppoon: Lat: -22.9523 Lng: 150.2093 xPixel 15028, yPixel: 9265
X/vertical movement: 15028 - 13465 = 1563
Y/horizontal movement: 9265 - 9728 = -463
Then, for that same trip, the following viewport sizes yield smooth pans; 1 pixel width or height less forces a hard pan: -
Viewport
Width: 1337 1435 1236
Height: 492 448 574
What is the formula for viewport pan borderline?
It should be obvious but I just can't see it
The only other information I have is: -
Google Title size at zero zoom = 256
The zoom I'm using is 6 = multiplier 64
X Pixel Formula = 256 * (0.5 + Longitude / 360) * 64
let siny = Math.sin((Latitude * Math.PI) / 180);
// Truncating to 0.9999 effectively limits latitude to 89.189. This is
// about a third of a tile past the edge of the world tile.
siny = Math.min(Math.max(siny, -0.9999), 0.9999);
Y Pixel Formula = 256 * (0.5 - Math.log((1 + siny) / (1 - siny)) / (4 * Math.PI))
Make any sense?
EDITEND
Please copy https://richardmaher.github.io/Brotkrumen/testmap.html if it makes life easier (has to be run locally due to API key)
See console for debugging info.
Shrink browser to Width 615px and you'll smooth scrolling/panning stop.
Ready to answer any other questions
See also https://issuetracker.google.com/issues/228349799
Can someone please explain in pseudocode, or at least less ambiguous language, the API Refence Manual definition for smooth transition requirements of the panTo() method: -
panTo panTo(latLng)
Parameters: latLng: LatLng|LatLngLiteral The
new center latitude/longitude of the map.
Return Value: None
Changes the center of the map to the given LatLng. If the change is less
than both the width and height of the map, the transition will be smoothly
animated.
Specifically, what is "the change" in this context?
Example: -
Zoom Level = 6
LatLng1 = lat: -31.9523, lng: 115.8613 Pixel X = 13464 Pixel Y = 9728
LatLng2 = lat: -33.8688, lng: 151.2093 Pixel X = 15073 Pixel Y = 9831
Current Map Center is LatLng1 and panning to LatLng2
I make the "change" to be horizontal 1609px and vertical 103px
If the map's DIV container is at least 616px wide and 344px high the pan is smooth if not it jumps.
Can someone please help me heuristicly marry up those figures with an algoithm?
panTo() changes the map's midpoint and if the coordinates of the midpoint changes within the viewport, the transition will be smoothly animated.
Viewport contains the recommended viewport for displaying the returned result, specified as two latitude, longitude values defining the southwest and northeast corner of the viewport bounding box. Generally the viewport is used to frame a result when displaying it to a user.
Ok I think the answer to the specific question is just down to rounding but I don't need to chase that one as, in the last month or so, Google has decouple its rehoming of the markers from the smooth pan test. It is now an arbitrary 100000px limit before it stops maintaing the DOM for markers.
The complete story can be found here (remember due to the API key hard-coding you need to copy testmap.html to your local file system before trying to run it.)
The TL;DR version and core logic is in this function: -
function makeDestCenter(){
console.log("Panning to new Center " + map.getZoom());
var home = map.getCenter();
var zoom = map.getZoom();
var scale = 1 << zoom;
var proj = map.getProjection();
var homePoint = proj.fromLatLngToPoint(home);
var startPixelX = Math.round(homePoint.x * scale);
var startPixelY = Math.round(homePoint.y * scale);
var destPoint = proj.fromLatLngToPoint(dest[destIndex]);
var destPixelX = Math.round(destPoint.x * scale);
var destPixelY = Math.round(destPoint.y * scale);
var xTrip = Math.abs(destPixelX - startPixelX);
var yTrip = Math.abs(destPixelY - startPixelY);
console.log("sX " + startPixelX + " dX " + destPixelX + " sY " + startPixelY + " dY " + destPixelY);
if ((xTrip > MAX_TRIP) || (yTrip > MAX_TRIP)) {
google.maps.event.addListenerOnce(map, 'idle', makeDestCenter);
map.setZoom(--zoom);
} else {
if (xTrip == TOO_SMALL && yTrip == TOO_SMALL) {
google.maps.event.addListenerOnce(map, 'idle', makeDestCenter);
map.setZoom(++zoom);
} else {
map.panTo(dest[destIndex]);
}
}
}
The markers will appear on the map if I pan the map, but if I zoom out and then zoom in on a copy of the map without the markers, they will not appear until I pan again.
Is it possible to change this so that zooming in and out will cause the markers to recalculate on the map?
L.map('map', {
'center': [0, 0],
'zoom': 0,
'worldCopyJump': true,
'layers': [
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
'attribution': 'Map data © OpenStreetMap contributors'
}),
L.marker([0, -135]),
L.marker([0, -90]),
L.marker([0, -45]),
L.marker([0, 0]),
L.marker([0, 45]),
L.marker([0, 90]),
L.marker([0, 135])
]
});
body {
margin: 0;
}
html, body, #map {
height: 100%
}
<link href="https://unpkg.com/leaflet/dist/leaflet.css" rel="stylesheet"/>
<script src="https://unpkg.com/leaflet/dist/leaflet-src.js"></script>
<div id="map"></div>
http://embed.plnkr.co/mWKc4M/
The markers will appear on the map if I pan the map
Specifically, this behaviour happens only when dragging the map (and not when panning the map via any other method, e.g. using keyboard shortcuts). This is because, internally, the worldCopyJump functionality is defined inside the Drag handler at src/map/handler/Map.Drag.js:
// TODO refactor, move to CRS
// #option worldCopyJump: Boolean = false
// With this option enabled, the map tracks when you pan to another "copy"
// of the world and seamlessly jumps to the original one so that all overlays
// like markers and vector layers are still visible.
worldCopyJump: false,
(Do note that Leaflet has an explanation of what map handlers are and how they work)
As the code stands now, the worldCopyJump functionality affects only the dragging handler, and works by resetting the drag offset (instead of the map center) every time the map dragging handler is about to be updated:
if (map.options.worldCopyJump) {
this._draggable.on('predrag', this._onPreDragWrap, this);
map.on('zoomend', this._onZoomEnd, this);
map.whenReady(this._onZoomEnd, this);
}
/* snip */
_onPreDragWrap: function () {
// TODO refactor to be able to adjust map pane position after zoom
var worldWidth = this._worldWidth,
halfWidth = Math.round(worldWidth / 2),
dx = this._initialWorldOffset,
x = this._draggable._newPos.x,
newX1 = (x - halfWidth + dx) % worldWidth + halfWidth - dx,
newX2 = (x + halfWidth + dx) % worldWidth - halfWidth - dx,
newX = Math.abs(newX1 + dx) < Math.abs(newX2 + dx) ? newX1 : newX2;
this._draggable._absPos = this._draggable._newPos.clone();
this._draggable._newPos.x = newX;
},
So, what to do? An option is to leverage wrapLatLng to reset the map center on every zoomend event, e.g.:
map.on('zoomend', function(ev){
map.panTo(map.wrapLatLng(map.getCenter()), {animate: false})
});
That should just work. See a working demo.
As an alternative, consider using https://gitlab.com/IvanSanchez/Leaflet.RepeatedMarkers , which will create copies of each marker every 360° of longitude.
I have a sidebar which shows the names of the markers in the current map view of a google map. The sidebar contents change as the map gets moved:
google.maps.event.addListener(map, 'bounds_changed', function() {
document.getElementById("list").innerHTML = "";
var mklen = mkrs.length,
a = 0,
bnds = map.getBounds();
for (a; a < mklen; a++) {
var themk = mkrs[a];
if (bnds.contains(themk.getPosition())) {
var myli = document.createElement("li");
myli.innerHTML = themk.title;
document.getElementById("list").appendChild(myli);
}
}
});
That's working OK, but the thing is that the bounds.contains() is very strict - if just the bottom tip of the marker is on the map (ie, you can't see 99% of it) it gets listed on the sidebar. What I'd like is to have just the markers that are completely shown pass that test.
There are a couple of approaches that I can think of and I can't believe that nobody else has come up against this problem, so I'm wondering if there is a preference out of the following:
take the bounds and recalculate them to be smaller than the actual bounds and use those new bounds for the bounds.contains() test
calculate where the edges of the marker icons are (I guess using fromDivPixelToLatLng) then check that both the ne AND sw corners are within the bounds and if so, list the item
Before you ask, I haven't tried either of those - I'm more looking for advice on which would be best or even possible, or if there is another way to do this. Here's a fiddle demonstrating the issue, in case it clarifies
In case anybody finds this later, I ended up recalculating the bounds - it seemed to be the approach that involved the least overhead. Here's the function:
function paddedBounds(npad, spad, epad, wpad) {
var SW = map.getBounds().getSouthWest();
var NE = map.getBounds().getNorthEast();
var topRight = map.getProjection().fromLatLngToPoint(NE);
var bottomLeft = map.getProjection().fromLatLngToPoint(SW);
var scale = Math.pow(2, map.getZoom());
var SWtopoint = map.getProjection().fromLatLngToPoint(SW);
var SWpoint = new google.maps.Point(((SWtopoint.x - bottomLeft.x) * scale) + wpad, ((SWtopoint.y - topRight.y) * scale) - spad);
var SWworld = new google.maps.Point(SWpoint.x / scale + bottomLeft.x, SWpoint.y / scale + topRight.y);
var pt1 = map.getProjection().fromPointToLatLng(SWworld);
var NEtopoint = map.getProjection().fromLatLngToPoint(NE);
var NEpoint = new google.maps.Point(((NEtopoint.x - bottomLeft.x) * scale) - epad, ((NEtopoint.y - topRight.y) * scale) + npad);
var NEworld = new google.maps.Point(NEpoint.x / scale + bottomLeft.x, NEpoint.y / scale + topRight.y);
var pt2 = map.getProjection().fromPointToLatLng(NEworld);
return new google.maps.LatLngBounds(pt1, pt2);
}
and you call it like this:
var padbnds = paddedBounds(50, 70, 100, 30);
specifying how much padding you want on the north, south, east and west edges of the map respectively
I am trying to implement something like this using leaflet.js, where the size of the circle remains the same on varying zoom levels. For example, if I want to depict the populations in different US counties, I would have circles of different radius represent different ranges of populations. They may overlap when zoomed out completely, but once we start zooming in, they tend to separate. So is there a way to do this using leaflet.js. I saw an issue raised, but I wasn't able to follow if it was fixed or not. Any help would be deeply appreciated.
For circles, just use circleMarker instead of circle: http://leafletjs.com/reference.html#circlemarker
I think you're gonna want to do something like this:
var map = L.map('map').setView([51.505, -0.09], 13);
var circle = L.circle([51.508, -0.11], 500, {
color: 'red',
fillColor: '#f03',
fillOpacity: 0.5
}).addTo(map);
var myZoom = {
start: map.getZoom(),
end: map.getZoom()
};
map.on('zoomstart', function(e) {
myZoom.start = map.getZoom();
});
map.on('zoomend', function(e) {
myZoom.end = map.getZoom();
var diff = myZoom.start - myZoom.end;
if (diff > 0) {
circle.setRadius(circle.getRadius() * 2);
} else if (diff < 0) {
circle.setRadius(circle.getRadius() / 2);
}
});
What I've done is simply initialize a map and a circle and created event listeners for the zoomstart and zoomend events. There's a myZoom object that records the zoom levels so you can find out whether or not the final zoom is in or out by simple subtraction. In the zoomEnd listener, you check that and change the circle radius based on whether the difference is greater or lesser than 0. We of course do nothing when it's 0. This is where I leave you to get more sophisticated with your results. But, I think this demonstrates how to do it.
Try this if you can't use circle markers for some reason:
// Determine the number of meters per pixel based on map zoom and latitude
const zoom = map.getZoom()
const lat = map.getCenter().lat
const metersPerPixel = 156543.03392 * Math.cos(lat * Math.PI / 180) / Math.pow(2, zoom)
// Multiply that by a factor based on how large the circle should appear on the screen
const radius = metersPerPixel * desiredRadiusInPixels
// Create the circle
const circle = L.circle(this.map.getCenter(), {radius: radius}))
I found the "meters per pixel" formula in this answer:
https://gis.stackexchange.com/a/127949/106283
Otherwise you can do like this, by adapting the exponent of the Math.pow() according to your needs
mymap.on('zoomend', function (e) {
var newRadius = Math.pow((20 - mymap.getZoom()), 4);
myCircle.setRadius(newRadius);
});
My map has several hundred markers within a city. Usually no more than a 20 mile radius.
I've read through the documentation and haven't found a way to set the init to automatically pan between every marker, no matter the distance.
The default behavior is to pan if close, jump if far.
I understand why they would do this since the map doesn't load the whole world at the selected zoom level and it could screw up if the distance was too great. However, I think it could handle 20 mile radius with minimal complaints.
If anyone has any ideas, I'd love to hear them.
Thanks
The threshold of the smooth panning does not depend on the distance between the current center and the new target. It depends on whether the change will require a full page scroll (horizontally and vertically) or not:
Quoting from the API Reference:
panTo(latLng:LatLng)
Changes the center of the map to the given LatLng. If the change is less than both the width and height of the map, the transition will be smoothly animated.
Therefore, as long as you are zoomed out such that your viewport is 20 miles in height and width, you should be guaranteed smooth panning for distances under 20 miles.
Here's a solution that pans smoothly and also allows for other click requests to be queue'd up while a previous pan is already in progress:
var panPath = []; // An array of points the current panning action will use
var panQueue = []; // An array of subsequent panTo actions to take
var STEPS = 50; // The number of steps that each panTo action will undergo
function panTo(newLat, newLng) {
if (panPath.length > 0) {
// We are already panning...queue this up for next move
panQueue.push([newLat, newLng]);
} else {
// Lets compute the points we'll use
panPath.push("LAZY SYNCRONIZED LOCK"); // make length non-zero - 'release' this before calling setTimeout
var curLat = map.getCenter().lat();
var curLng = map.getCenter().lng();
var dLat = (newLat - curLat)/STEPS;
var dLng = (newLng - curLng)/STEPS;
for (var i=0; i < STEPS; i++) {
panPath.push([curLat + dLat * i, curLng + dLng * i]);
}
panPath.push([newLat, newLng]);
panPath.shift(); // LAZY SYNCRONIZED LOCK
setTimeout(doPan, 20);
}
}
function doPan() {
var next = panPath.shift();
if (next != null) {
// Continue our current pan action
map.panTo( new google.maps.LatLng(next[0], next[1]));
setTimeout(doPan, 20 );
} else {
// We are finished with this pan - check if there are any queue'd up locations to pan to
var queued = panQueue.shift();
if (queued != null) {
panTo(queued[0], queued[1]);
}
}
}
We developed a workaround to smoothly animate the panTo in all cases.
Basically in cases that the native panTo will not animate smoothly, we zoom out, panTo and zoom in to the destination location.
To use the code below, call smoothlyAnimatePanTo passing the map instance as first parameter and the destination latLng as second parameter.
There is a jsfiddle to demonstrate this solution in action here. Just edit the script tag to put your own google maps javascript api key.
Any comments and contributions will be welcome.
/**
* Handy functions to project lat/lng to pixel
* Extracted from: https://developers.google.com/maps/documentation/javascript/examples/map-coordinates
**/
function project(latLng) {
var TILE_SIZE = 256
var siny = Math.sin(latLng.lat() * Math.PI / 180)
// Truncating to 0.9999 effectively limits latitude to 89.189. This is
// about a third of a tile past the edge of the world tile.
siny = Math.min(Math.max(siny, -0.9999), 0.9999)
return new google.maps.Point(
TILE_SIZE * (0.5 + latLng.lng() / 360),
TILE_SIZE * (0.5 - Math.log((1 + siny) / (1 - siny)) / (4 * Math.PI)))
}
/**
* Handy functions to project lat/lng to pixel
* Extracted from: https://developers.google.com/maps/documentation/javascript/examples/map-coordinates
**/
function getPixel(latLng, zoom) {
var scale = 1 << zoom
var worldCoordinate = project(latLng)
return new google.maps.Point(
Math.floor(worldCoordinate.x * scale),
Math.floor(worldCoordinate.y * scale))
}
/**
* Given a map, return the map dimension (width and height)
* in pixels.
**/
function getMapDimenInPixels(map) {
var zoom = map.getZoom()
var bounds = map.getBounds()
var southWestPixel = getPixel(bounds.getSouthWest(), zoom)
var northEastPixel = getPixel(bounds.getNorthEast(), zoom)
return {
width: Math.abs(southWestPixel.x - northEastPixel.x),
height: Math.abs(southWestPixel.y - northEastPixel.y)
}
}
/**
* Given a map and a destLatLng returns true if calling
* map.panTo(destLatLng) will be smoothly animated or false
* otherwise.
*
* optionalZoomLevel can be optionally be provided and if so
* returns true if map.panTo(destLatLng) would be smoothly animated
* at optionalZoomLevel.
**/
function willAnimatePanTo(map, destLatLng, optionalZoomLevel) {
var dimen = getMapDimenInPixels(map)
var mapCenter = map.getCenter()
optionalZoomLevel = !!optionalZoomLevel ? optionalZoomLevel : map.getZoom()
var destPixel = getPixel(destLatLng, optionalZoomLevel)
var mapPixel = getPixel(mapCenter, optionalZoomLevel)
var diffX = Math.abs(destPixel.x - mapPixel.x)
var diffY = Math.abs(destPixel.y - mapPixel.y)
return diffX < dimen.width && diffY < dimen.height
}
/**
* Returns the optimal zoom value when animating
* the zoom out.
*
* The maximum change will be currentZoom - 3.
* Changing the zoom with a difference greater than
* 3 levels will cause the map to "jump" and not
* smoothly animate.
*
* Unfortunately the magical number "3" was empirically
* determined as we could not find any official docs
* about it.
**/
function getOptimalZoomOut(map, latLng, currentZoom) {
if(willAnimatePanTo(map, latLng, currentZoom - 1)) {
return currentZoom - 1
} else if(willAnimatePanTo(map, latLng, currentZoom - 2)) {
return currentZoom - 2
} else {
return currentZoom - 3
}
}
/**
* Given a map and a destLatLng, smoothly animates the map center to
* destLatLng by zooming out until distance (in pixels) between map center
* and destLatLng are less than map width and height, then panTo to destLatLng
* and finally animate to restore the initial zoom.
*
* optionalAnimationEndCallback can be optionally be provided and if so
* it will be called when the animation ends
**/
function smoothlyAnimatePanToWorkarround(map, destLatLng, optionalAnimationEndCallback) {
var initialZoom = map.getZoom(), listener
function zoomIn() {
if(map.getZoom() < initialZoom) {
map.setZoom(Math.min(map.getZoom() + 3, initialZoom))
} else {
google.maps.event.removeListener(listener)
//here you should (re?)enable only the ui controls that make sense to your app
map.setOptions({draggable: true, zoomControl: true, scrollwheel: true, disableDoubleClickZoom: false})
if(!!optionalAnimationEndCallback) {
optionalAnimationEndCallback()
}
}
}
function zoomOut() {
if(willAnimatePanTo(map, destLatLng)) {
google.maps.event.removeListener(listener)
listener = google.maps.event.addListener(map, 'idle', zoomIn)
map.panTo(destLatLng)
} else {
map.setZoom(getOptimalZoomOut(map, destLatLng, map.getZoom()))
}
}
//here you should disable all the ui controls that your app uses
map.setOptions({draggable: false, zoomControl: false, scrollwheel: false, disableDoubleClickZoom: true})
map.setZoom(getOptimalZoomOut(map, destLatLng, initialZoom))
listener = google.maps.event.addListener(map, 'idle', zoomOut)
}
function smoothlyAnimatePanTo(map, destLatLng) {
if(willAnimatePanTo(map, destLatLng)) {
map.panTo(destLatLng)
} else {
smoothlyAnimatePanToWorkarround(map, destLatLng)
}
}
See this other SO answer about using javascript's setInterval function to create a periodic function that calls panBy on your map: Can Google Maps be set to a slow constant pan? Like a globe revolution?
This can be used to pan the map by x pixels on each call to panBy, allowing you to slow down the panBy rate (since you are only telling gmaps to panTo a short distance).
As Daniel has mentioned, the built-in panTo() function will not work for you if the two points are too far apart. You can manually animate it yourself if that's the case though: for each zoom level, figure out the distance covered by say 100 pixels. Now, when you have to pan to a point, you can use this information to figure out if the panTo() funciton will animate or jump. If the distance moved is so big that it will not animate, you should do the animation manually - compute some intermediate waypoints between your current map center and your destination, and pan to them in sequence.
#tato.rodrigo
I don't have enough reputation to post as an answer so am posting as a reply to Tato here as his plugin works well for me and is exactly what I needed but has a bug (I use it as a dependency so the map variable is passed through the function)
You need to pass map to function getOptimalZoomOut(latLng, currentZoom) {}
as you use the map variable inside that function.
like this: function getOptimalZoomOut(latLng, currentZoom, map) {}
and later: map.setZoom(getOptimalZoomOut(destLatLng, initialZoom)); pass it in: map.setZoom(getOptimalZoomOut(destLatLng, initialZoom, map)); and maybe another stray one.