89

I have a Google map with a semi transparent panel covering a portion of the area. I would like to adjust the center point of the map to take into account the portion of the map that is partially obscured. See the image below. Ideally, where the crosshairs and pin are placed would be the center point of the map.

I hope that makes sense.

The reason is simple: When you zoom it needs to center the map over the crosshair rather than at 50% 50%. Also, I will be plotting markers on the map and moving through them in sequence. When the map centers on them, they also need to be at the offset position.

Mockup of the map I am building

starball
  • 20,030
  • 7
  • 43
  • 238
will
  • 4,557
  • 6
  • 32
  • 33
  • You are going to have to clarify your question. What does "take into account the portion of the map that is partially obscured" mean? And what does "rather than at 50% 50%" mean? Want to help, but can't figure out what you are trying to achieve. – Sean Mickey May 18 '12 at 18:25
  • Hi, Can you please give the url of working example because i need some help for creating a moving ploy line along with route line – SP Singh Apr 06 '13 at 12:34

10 Answers10

90

This is not particularly difficult once you find the relevant previous answer.

You need to convert the centre of the map to its world co-ordinates, find where the map needs to be centered to put the apparent centre where you want it, and re-centre the map using the real centre.

The API will always centre the map on the centre of the viewport, so you need to be careful if you use map.getCenter() as it will return the real centre, not the apparent centre. I suppose it would be possible to overload the API so that its getCenter() and setCenter() methods are replaced, but I haven't done that.

Code below. Example online. In the example, clicking the button shifts the centre of the map (there's a road junction there) down 100px and left 200px.

function offsetCenter(latlng, offsetx, offsety) {

    // latlng is the apparent centre-point
    // offsetx is the distance you want that point to move to the right, in pixels
    // offsety is the distance you want that point to move upwards, in pixels
    // offset can be negative
    // offsetx and offsety are both optional

    var scale = Math.pow(2, map.getZoom());

    var worldCoordinateCenter = map.getProjection().fromLatLngToPoint(latlng);
    var pixelOffset = new google.maps.Point((offsetx/scale) || 0,(offsety/scale) ||0);

    var worldCoordinateNewCenter = new google.maps.Point(
        worldCoordinateCenter.x - pixelOffset.x,
        worldCoordinateCenter.y + pixelOffset.y
    );

    var newCenter = map.getProjection().fromPointToLatLng(worldCoordinateNewCenter);

    map.setCenter(newCenter);

}
Community
  • 1
  • 1
Andrew Leach
  • 12,945
  • 1
  • 40
  • 47
  • Is it possible to get this code to fire when the page loads so that the map shows as offset when first viewed? – Darren Cook Jun 01 '12 at 11:26
  • @DarrenCook: Of course. Just use this instead of `map.setCenter()`. All this code does is recalculate what the map is told do do, and then it does its own `map.setCenter()`. – Andrew Leach Jun 01 '12 at 11:33
  • Thanks Andrew, so in my code I took addMarkerFromAdress(latLng, title) and replaced with function offsetCenter(latlng,offsetx,offsety) and it all still works - but how do I get the value for offsety (243px) into the function? – Darren Cook Jun 01 '12 at 11:55
  • 30
    Better use `map.panBy(250, 0);` – Alexander Shvetsov Oct 29 '13 at 17:03
  • @AlexanderShvetsov `panBy` moves the map, which may not be desirable. The `offsetCenter` function doesn't do that; it provides a direct replacement for `setCenter` without panning the map. – Andrew Leach Oct 31 '13 at 13:24
  • 2
    I found that the `nw` var is never used by this script and at least in my use case could be removed without any issue. I didn't try this by itself though since i have a bunch of other methods already setting the bounds and all that. – hellatan Dec 01 '13 at 04:55
  • I had issues with getProjection(). Found this better solution: http://stackoverflow.com/a/12608699/457850 which works first go – dtbaker Dec 15 '13 at 12:50
  • I'd say the reason's for issues with getProjection is the variable `map` not being defined - this code seems to assume `map` has been set to the google maps instance in the scope the function is defined in. I added `map` as a parameter (as there are multiple maps coexisting in our solution) and passed it with the function. – Chris O'Kelly Mar 11 '15 at 23:57
  • @AndrewLeach after trying your code, I think you're flipping the sign on the offset.y. For me I also have to subtract it if I want to push the center down by a certain number of pixels. – Ben M. Jun 01 '17 at 20:19
  • @BenM. "clicking the button shifts the centre of the map (there's a road junction there) down 100px and left 200px." That's intended, and it still happens. – Andrew Leach Jun 01 '17 at 21:53
  • @AndrewLeach, I see what you're saying, I guess "down" to mean would mean that the latitude would decrease. In your example, If I start zoomed in on Sheffield Green, after a few button clicks I'm at Crowborough, which is NW of Sheffield Green (which to me is "up" and "right") but I can understand that you're saying the map is going "down and left". – Ben M. Jun 02 '17 at 18:28
  • Hello, I tried to copy your code for the cordova google maps plugin but every time I call this method when the user presses a 'center' button it gives me a different position. I just want the center of the map to be upper so the user's position marker is nearer the bottom of the screen for when he navigates. Also, it doesn't work for me if I use the 'scale' variable My code is this, help please: https://github.com/mapsplugin/cordova-plugin-googlemaps/issues/2551#issuecomment-458087325 – Wrong Jan 30 '19 at 13:35
  • Awesome Andrew – Manuel Alanis Sep 20 '19 at 19:40
  • Hi, I am working on something similar. I have one question though, how do I get the latlng of the apparent center of the map, I am centering that marker using css and only have offset values in px. Thanks in advance – Mr94 Dec 20 '19 at 05:41
77

Also take a look at the panBy(x:number, y:number) function on the maps object.

The documentation mentions this about the function:

Changes the center of the map by the given distance in pixels. If the distance is less than both the width and height of the map, the transition will be smoothly animated. Note that the map coordinate system increases from west to east (for x values) and north to south (for y values).

Just use it like this:

mapsObject.panBy(200, 100)
Kah Tang
  • 1,664
  • 12
  • 19
12

Here's an example of solving the problem using panBy() method of the maps API: http://jsfiddle.net/upsidown/2wej9smf/

Konstantine Kalbazov
  • 2,643
  • 26
  • 29
12

Just found another simplest solution. In case you're using fitBounds method, you can pass optional second argument to its. This argument is padding, that will be considered while fitting bounds.

// pass single side:
map.fitBounds(bounds, { left: 1000 })

// OR use Number:
map.fitBounds(bounds, 20)

Further reading: official docs.

Nickensoul
  • 435
  • 6
  • 9
9

Here's a simpler method that might be more useful in responsive design since you can use percentages instead of pixels. No world coordinates, no LatLngs to Points!

var center;  // a latLng
var offsetX = 0.25; // move center one quarter map width left
var offsetY = 0.25; // move center one quarter map height down

var span = map.getBounds().toSpan(); // a latLng - # of deg map spans

var newCenter = { 
    lat: center.lat() + span.lat()*offsetY,
    lng: center.lng() + span.lng()*offsetX
};

map.panTo(newCenter);  // or map.setCenter(newCenter);
brycewjohnson
  • 99
  • 1
  • 4
  • Tried this on InfoBox and it works! (although the pixelOffset still needs to be fixed) – rainy Dec 18 '15 at 07:22
  • 1
    Great answer for both the simplicity of the solution AND taking responsive design into consideration – Daniel Tonon Mar 02 '16 at 07:19
  • 1
    I get an Uncaught TypeError: Cannot read property 'toSpan' of undefined – Mike Kormendy Jul 18 '16 at 01:02
  • can we get the newCenter lat long ? – Mr. Tomar Aug 03 '16 at 11:29
  • 1
    This would not work if there is a need to change the zoom in the same operation. Also, it would not work accurately if the destination (new) center is located geographically far from the current center. Even at the same zoom level, 0.25 of the width of the screen represents a different distance at the equator than in Norway, for example, due to the Mercator projection used. Projecting the final coordinates is more robust. – pscl May 06 '17 at 13:45
6

Andrew's is the answer. However, in my case, map.getBounds() kept returning undefined. I fixed it waiting for the bounds_changed event and then call the function to offset the center. Like so:

var center_moved = false;
google.maps.event.addListener(map, 'bounds_changed', function() {
  if(!center_moved){
    offsetCenter(map.getCenter(), 250, -200);
    center_moved = true; 
  }
});
Nahuel
  • 3,555
  • 4
  • 21
  • 28
4

Old question, I know. But how about a more CSS-centric way?

http://codepen.io/eddyblair/pen/VjpNQQ

What I did was:

  • Wrap the map and overlay in a container with overflow: hidden

  • Overlaid the overlay with position: absolute

  • Extended the map's apparent width by the overlay width (plus any padding and offset) by setting a negative margin-left.

  • Then in order to comply with https://www.google.com/permissions/geoguidelines/attr-guide.html positioned the widgets and attribution divs.

This way the centre of the map lies in line with the centre of the desired area. The js is just standard map js.

Repositioning the icons for street-view is an exercise for the reader :)


If you want the overlay on the left, just change line 24 margin-left to margin-right and line 32 right to left.

Mardoxx
  • 4,372
  • 7
  • 41
  • 67
  • I really like this approach. Keeps the Angular/javascript nice and clean. – Various Artist Mar 29 '17 at 01:33
  • Thanks :) This will, of course, need updating whenever google change their styles. I put comments before the CSS which moves the attributions and controls etc.. which should give an idea of how I did it if it does need modifying. Additional CSS will need creating to sort out street view. – Mardoxx Mar 29 '17 at 09:10
3

After extensive searching I could not find a way to do this that also included zoom. Thankfully a clever chap has figured it out. There is also a fiddle here

'use strict';

const TILE_SIZE = {
  height: 256,
  width: 256
}; // google World tile size, as of v3.22
const ZOOM_MAX = 21; // max google maps zoom level, as of v3.22
const BUFFER = 15; // edge buffer for fitting markers within viewport bounds

const mapOptions = {
  zoom: 14,
  center: {
    lat: 34.075328,
    lng: -118.330432
  },
  options: {
    mapTypeControl: false
  }
};
const markers = [];
const mapDimensions = {};
const mapOffset = {
  x: 0,
  y: 0
};
const mapEl = document.getElementById('gmap');
const overlayEl = document.getElementById('overlay');
const gmap = new google.maps.Map(mapEl, mapOptions);

const updateMapDimensions = () => {
  mapDimensions.height = mapEl.offsetHeight;
  mapDimensions.width = mapEl.offsetWidth;
};

const getBoundsZoomLevel = (bounds, dimensions) => {
  const latRadian = lat => {
    let sin = Math.sin(lat * Math.PI / 180);
    let radX2 = Math.log((1 + sin) / (1 - sin)) / 2;
    return Math.max(Math.min(radX2, Math.PI), -Math.PI) / 2;
  };
  const zoom = (mapPx, worldPx, fraction) => {
    return Math.floor(Math.log(mapPx / worldPx / fraction) / Math.LN2);
  };
  const ne = bounds.getNorthEast();
  const sw = bounds.getSouthWest();
  const latFraction = (latRadian(ne.lat()) - latRadian(sw.lat())) / Math.PI;
  const lngDiff = ne.lng() - sw.lng();
  const lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360;
  const latZoom = zoom(dimensions.height, TILE_SIZE.height, latFraction);
  const lngZoom = zoom(dimensions.width, TILE_SIZE.width, lngFraction);
  return Math.min(latZoom, lngZoom, ZOOM_MAX);
};

const getBounds = locations => {
  let northeastLat;
  let northeastLong;
  let southwestLat;
  let southwestLong;
  locations.forEach(function(location) {
    if (!northeastLat) {
      northeastLat = southwestLat = location.lat;
      southwestLong = northeastLong = location.lng;
      return;
    }
    if (location.lat > northeastLat) northeastLat = location.lat;
    else if (location.lat < southwestLat) southwestLat = location.lat;
    if (location.lng < northeastLong) northeastLong = location.lng;
    else if (location.lng > southwestLong) southwestLong = location.lng;
  });
  const northeast = new google.maps.LatLng(northeastLat, northeastLong);
  const southwest = new google.maps.LatLng(southwestLat, southwestLong);
  const bounds = new google.maps.LatLngBounds();
  bounds.extend(northeast);
  bounds.extend(southwest);
  return bounds;
};

const zoomWithOffset = shouldZoom => {
  const currentzoom = gmap.getZoom();
  const newzoom = shouldZoom ? currentzoom + 1 : currentzoom - 1;
  const offset = {
    x: shouldZoom ? -mapOffset.x / 4 : mapOffset.x / 2,
    y: shouldZoom ? -mapOffset.y / 4 : mapOffset.y / 2
  };
  const newCenter = offsetLatLng(gmap.getCenter(), offset.x, offset.y);
  if (shouldZoom) {
    gmap.setZoom(newzoom);
    gmap.panTo(newCenter);
  } else {
    gmap.setCenter(newCenter);
    gmap.setZoom(newzoom);
  }
};

const setMapBounds = locations => {
  updateMapDimensions();
  const bounds = getBounds(locations);
  const dimensions = {
    width: mapDimensions.width - mapOffset.x - BUFFER * 2,
    height: mapDimensions.height - mapOffset.y - BUFFER * 2
  };
  const zoomLevel = getBoundsZoomLevel(bounds, dimensions);
  gmap.setZoom(zoomLevel);
  setOffsetCenter(bounds.getCenter());
};

const offsetLatLng = (latlng, offsetX, offsetY) => {
  offsetX = offsetX || 0;
  offsetY = offsetY || 0;
  const scale = Math.pow(2, gmap.getZoom());
  const point = gmap.getProjection().fromLatLngToPoint(latlng);
  const pixelOffset = new google.maps.Point((offsetX / scale), (offsetY / scale));
  const newPoint = new google.maps.Point(
    point.x - pixelOffset.x,
    point.y + pixelOffset.y
  );
  return gmap.getProjection().fromPointToLatLng(newPoint);
};

const setOffsetCenter = latlng => {
  const newCenterLatLng = offsetLatLng(latlng, mapOffset.x / 2, mapOffset.y / 2);
  gmap.panTo(newCenterLatLng);
};

const locations = [{
  name: 'Wilshire Country Club',
  lat: 34.077796,
  lng: -118.331151
}, {
  name: '301 N Rossmore Ave',
  lat: 34.077146,
  lng: -118.327805
}, {
  name: '5920 Beverly Blvd',
  lat: 34.070281,
  lng: -118.331831
}];

locations.forEach(function(location) {
  let marker = new google.maps.Marker({
    position: new google.maps.LatLng(location.lat, location.lng),
    title: location.name
  })
  marker.setMap(gmap);
  markers.push(marker);
});

mapOffset.x = overlayEl.offsetWidth;

document.zoom = bool => zoomWithOffset(bool);
document.setBounds = () => setMapBounds(locations);
section {
  height: 180px;
  margin-bottom: 15px;
  font-family: sans-serif;
  color: grey;
}
figure {
  position: relative;
  margin: 0;
  width: 100%;
  height: 100%;
}
figcaption {
  position: absolute;
  left: 15px;
  top: 15px;
  width: 120px;
  padding: 15px;
  background: white;
  box-shadow: 0 2px 5px rgba(0, 0, 0, .3);
}
gmap {
  display: block;
  height: 100%;
}
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js"></script>

<section>
  <figure>
    <gmap id="gmap"></gmap>
    <figcaption id="overlay">
      <h4>Tile Overlay</h4>
      <p>To be avoided by the map!</p>
    </figcaption>
  </figure>
</section>
<button onclick="zoom(true)">zoom in</button>
<button onclick="zoom(false)">zoom out</button>
<button onclick="setBounds()">set bounds</button>
Chris
  • 26,744
  • 48
  • 193
  • 345
1

Another approach when it comes to offsetting a route or a group of markers can be found here:

https://stackoverflow.com/a/26192440/1238965

It still uses the fromLatLngToPoint() method described in @Andrew Leach answer.

Community
  • 1
  • 1
MrUpsidown
  • 21,592
  • 15
  • 77
  • 131
0

Below is an example of SIMPLE use for:

  • for DESKTOP version when the left side is OVERLAY with a width of 500
  • for the MOBILE version when the bottom of the page is OVERLAY with a height of 250

The solution will also work smoothly when the screen size changes live.

Requires that query be defined in css with @media screen and (min-width: 1025px) and @media screen and (max-width: 1024px)

Code:

function initMap() {
    map = new google.maps.Map(document.getElementById("map"), {
        zoom: 13
    });

    bounds = new google.maps.LatLngBounds();
    // add markers to bounds

    // Initial bounds
    defineBounds()

    // Trigger resize browser and set new bounds
    google.maps.event.addDomListener(window, 'resize', function() {
        defineBounds()
    });
}

function defineBounds() {
    if (window.matchMedia("(min-width:1025px)").matches) {
        map.fitBounds(bounds, {
            top: 100,
            right: 100,
            left: 600, // 500 + 100
            bottom: 100
        });
    } else {
        map.fitBounds(bounds, {
            top: 80,
            right: 80,
            left: 80,
            bottom: 330 // 250 + 80
        });
    }
}

The solution can be customized for OVERLAY located on each side of the screen

DeveloperApps
  • 763
  • 7
  • 16