Drawing a circle Google Static Maps - google-maps
I have a Google Maps Circle drawn on v3 api. When the user has plotted there circle (or polygon if they choose), they can save the data to the server. If the user has picked a radial search, the Centre coordinates and the radius in feet is stored to the database. This means when the user reloads his search, it can pull through the circle again (like below).
I'm having 1 problem, however, which is when the user selects what search they would like to use. It loads the polygon fine, if they drew a polygon, and if it's a circle it pulls through the marker on the center. However what I need is a function in static maps to draw a circle.
A bit late in the game, but nothing I found solved my issue (serverside php only, no javascript).
I ended up getting there in the end and have detailed my method here: http://jomacinc.com/map-radius/ and the short version is below.
This PHP function will return an encoded polyline string of lat/lng points in a circle around the specified point, and at the specified radius. The function requires Gabriel Svennerberg’s PHP polyline encoding class available here (http://www.svennerberg.com/examples/polylines/PolylineEncoder.php.txt).
function GMapCircle($Lat,$Lng,$Rad,$Detail=8){
$R = 6371;
$pi = pi();
$Lat = ($Lat * $pi) / 180;
$Lng = ($Lng * $pi) / 180;
$d = $Rad / $R;
$points = array();
$i = 0;
for($i = 0; $i <= 360; $i+=$Detail):
$brng = $i * $pi / 180;
$pLat = asin(sin($Lat)*cos($d) + cos($Lat)*sin($d)*cos($brng));
$pLng = (($Lng + atan2(sin($brng)*sin($d)*cos($Lat), cos($d)-sin($Lat)*sin($pLat))) * 180) / $pi;
$pLat = ($pLat * 180) /$pi;
$points[] = array($pLat,$pLng);
endfor;
require_once('PolylineEncoder.php');
$PolyEnc = new PolylineEncoder($points);
$EncString = $PolyEnc->dpEncode();
return $EncString['Points'];
}
You can now the use the above function to create a static map.
/* set some options */
$MapLat = '-42.88188'; // latitude for map and circle center
$MapLng = '147.32427'; // longitude as above
$MapRadius = 100; // the radius of our circle (in Kilometres)
$MapFill = 'E85F0E'; // fill colour of our circle
$MapBorder = '91A93A'; // border colour of our circle
$MapWidth = 640; // map image width (max 640px)
$MapHeight = 480; // map image height (max 640px)
/* create our encoded polyline string */
$EncString = GMapCircle($MapLat,$MapLng, $MapRadius);
/* put together the static map URL */
$MapAPI = 'http://maps.google.com.au/maps/api/staticmap?';
$MapURL = $MapAPI.'center='.$MapLat.','.$MapLng.'&size='.$MapWidth.'x'.$MapHeight.'&maptype=roadmap&path=fillcolor:0x'.$MapFill.'33%7Ccolor:0x'.$MapBorder.'00%7Cenc:'.$EncString.'&sensor=false';
/* output an image tag with our map as the source */
echo '<img src="'.$MapURL.'" />'
function GMapCircle(lat,lng,rad,detail=8){
var uri = 'https://maps.googleapis.com/maps/api/staticmap?';
var staticMapSrc = 'center=' + lat + ',' + lng;
staticMapSrc += '&size=100x100';
staticMapSrc += '&path=color:0xff0000ff:weight:1';
var r = 6371;
var pi = Math.PI;
var _lat = (lat * pi) / 180;
var _lng = (lng * pi) / 180;
var d = (rad/1000) / r;
var i = 0;
for(i = 0; i <= 360; i+=detail) {
var brng = i * pi / 180;
var pLat = Math.asin(Math.sin(_lat) * Math.cos(d) + Math.cos(_lat) * Math.sin(d) * Math.cos(brng));
var pLng = ((_lng + Math.atan2(Math.sin(brng) * Math.sin(d) * Math.cos(_lat), Math.cos(d) - Math.sin(_lat) * Math.sin(pLat))) * 180) / pi;
pLat = (pLat * 180) / pi;
staticMapSrc += "|" + pLat + "," + pLng;
}
return uri + encodeURI(staticMapSrc);}
Javascript version
Based on the answer from Jomac, Here is a Java/Android version of the same code.
It uses the PolyUtil class from Google Maps Android API Utility Library to encode the path.
import android.location.Location;
import com.google.android.gms.maps.model.LatLng;
import com.google.maps.android.PolyUtil;
import java.util.ArrayList;
public class GoogleStaticMapsAPIServices
{
private static final double EARTH_RADIUS_KM = 6371;
private static String GOOGLE_STATIC_MAPS_API_KEY = "XXXXXXXXXXXXX";
public static String getStaticMapURL(Location location, int radiusMeters)
{
String pathString = "";
if (radiusMeters > 0)
{
// Add radius path
ArrayList<LatLng> circlePoints = getCircleAsPolyline(location, radiusMeters);
if (circlePoints.size() > 0)
{
String encodedPathLocations = PolyUtil.encode(circlePoints);
pathString = "&path=color:0x0000ffff%7Cweight:1%7Cfillcolor:0x0000ff80%7Cenc:" + encodedPathLocations;
}
}
String staticMapURL = "https://maps.googleapis.com/maps/api/staticmap?size=640x320&markers=color:red%7C" +
location.getLatitude() + "," + location.getLongitude() +
pathString +
"&key=" + GOOGLE_STATIC_MAPS_API_KEY;
return staticMapURL;
}
private static ArrayList<LatLng> getCircleAsPolyline(Location center, int radiusMeters)
{
ArrayList<LatLng> path = new ArrayList<>();
double latitudeRadians = center.getLatitude() * Math.PI / 180.0;
double longitudeRadians = center.getLongitude() * Math.PI / 180.0;
double radiusRadians = radiusMeters / 1000.0 / EARTH_RADIUS_KM;
double calcLatPrefix = Math.sin(latitudeRadians) * Math.cos(radiusRadians);
double calcLatSuffix = Math.cos(latitudeRadians) * Math.sin(radiusRadians);
for (int angle = 0; angle < 361; angle += 10)
{
double angleRadians = angle * Math.PI / 180.0;
double latitude = Math.asin(calcLatPrefix + calcLatSuffix * Math.cos(angleRadians));
double longitude = ((longitudeRadians + Math.atan2(Math.sin(angleRadians) * Math.sin(radiusRadians) * Math.cos(latitudeRadians), Math.cos(radiusRadians) - Math.sin(latitudeRadians) * Math.sin(latitude))) * 180) / Math.PI;
latitude = latitude * 180.0 / Math.PI;
path.add(new LatLng(latitude, longitude));
}
return path;
}
}
I think it is not possible to draw a circle on a static Google map. You would need to approximate the circle by a polyline (best in encoded format). This has already been mentioned in Stackoverflow and it is demonstrated e.g. by Free Map Tools.
Sharing my C# version
private string GMapCircle(double lat, double lng, double rad, int detail = 8)
{
const string uri = "https://maps.googleapis.com/maps/api/staticmap?";
var staticMapSrc = "center=" + lat + "," + lng;
staticMapSrc += "&zoom=16";
staticMapSrc += "&maptype=satellite";
staticMapSrc += "&key=[YOURKEYHERE]";
staticMapSrc += "&size=640x426";
staticMapSrc += "&path=color:0xff0000ff:weight:1";
const int r = 6371;
const double pi = Math.PI;
var latAux = (lat * pi) / 180;
var longAux = (lng * pi) / 180;
var d = (rad / 1000) / r;
var i = 0;
if (rad > 0)
{
for (i = 0; i <= 360; i += detail)
{
var brng = i * pi / 180;
var pLat = Math.Asin(Math.Sin(latAux) * Math.Cos(d) + Math.Cos(latAux) * Math.Sin(d) * Math.Cos(brng));
var pLng = ((longAux + Math.Atan2(Math.Sin(brng) * Math.Sin(d) * Math.Cos(latAux), Math.Cos(d) - Math.Sin(latAux) * Math.Sin(pLat))) * 180) / pi;
pLat = (pLat * 180) / pi;
staticMapSrc += "|" + pLat + "," + pLng;
}
}
else
{
//TODO - Add marker
}
return uri + staticMapSrc;
}
This solution uses the significantly more versatile Canvas API to draw over the map image . All code is in Typescript, so simply remove type declarations if you're using Javascript.
ADVANGES OF USING CANVAS:
Its easier to draw shapes on.
Those shapes can also be revised without re-requesting the map image from Google.
The drawing 'layer' can be serialized independently of the underlying map image.
USAGE:
// DEFINE BASIC VARIABLES
const latitude: -34.3566871,
const longitude: 18.4967666
const mapZoom = 12;
const imageWidth: 100;
const imageHeight: 100;
// INVOKE UTILITY FUNCTION
savePlaceImage({
// SET BASIC PROPS
latitude,
longitude,
mapZoom,
imageWidth,
imageHeight,
fileName: 'Cape Point',
// DRAW IMAGE USING CANVAS API
draw: ctx => {
// draw location as dot
ctx.fillStyle = '#FF3366';
ctx.beginPath();
ctx.arc(imageWidth / 2, imageHeight / 2, 10, 0, 2 * Math.PI);
ctx.fill();
// draw circle around location with 1 kilometer radius
ctx.strokeStyle = '#0000FF';
ctx.beginPath();
ctx.arc(imageWidth / 2, imageHeight / 2, pixelsPerMeter(latitude) * 1000, 0, 2 * Math.PI);
ctx.stroke();
}
})
UTILITIES:
function savePlaceImage(
config: {
latitude: number,
longitude: number,
mapZoom: number,
imageWidth: number,
imageHeight: number,
fileName: string,
draw: (ctx: CanvasRenderingContext2D) => void,
},
) {
// DOWNLOAD MAP IMAGE FROM GOOGLE'S STATIC MAPS API AS A BLOB
return from(axios.get<Blob>(`https://maps.googleapis.com/maps/api/staticmap`, {
params: {
key: GOOGLE_MAPS_API_KEY,
size: `${config.imageWidth}x${config.imageHeight}`,
zoom: `${config.mapZoom}`,
center: `${config.latitude},${config.longitude}`,
style: 'feature:all|element:labels|visibility:off',
},
responseType: 'blob',
// CONVERT BLOB TO BASE64 ENCODED STRING
}).then(response => {
const reader = new FileReader();
reader.readAsDataURL(response.data);
return new Promise<string>(resolve => reader.onloadend = () => resolve(reader.result as string));
// CREATE HTML IMG ELEMENT, SET IT'S SRC TO MAP IMAGE, AND WAIT FOR IT TO LOAD
}).then(response => {
const image = document.createElement('img');
image.src = response;
return new Promise<HTMLImageElement>(resolve => image.onload = () => resolve(image));
// CREATE HTML CANVAS ELEMENT, THEN DRAW ON TOP OF CANVAS USING CANVAS API, THEN CONVERT TO BLOB
}).then(image => {
const canvas = document.createElement('canvas');
canvas.width = config.imageWidth;
canvas.height = config.imageHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
config.draw(ctx);
return new Promise<Blob>(resolve => canvas.toBlob(blob => resolve(blob)));
// ATTACH BLOB TO HTML FORM WHICH CONVERTS IT TO A FILE TO BE POSTED, THEN SEND FILE TO SERVER
}).then(blob => {
const form = new FormData();
form.append('blob', blob, `${config.fileName}.png`);
const file = form.get('blob') as File;
return axios.post<{ file }>('https://www.my-api.com/save-image', form);
}));
}
function pixelsPerMeter(latitude: number) {
const radiusOfEarthInKilometers = 6371;
return Math.cos(latitude * Math.PI / 180) * 2 * Math.PI * radiusOfEarthInKilometers / (256 * Math.pow(2, 12));
}
python version, polyline library used for encoding the polygon
import math, polyline
def g_map_circle(lat,lng,radius,detail=8):
points = []
r = 6371
pi = math.pi
_lat = (lat * pi) /180
_lng = (lng * pi) /180
d = radius / r
i = 0
while i <= 360:
i = i + detail
brng = i * pi /180
p_lat = math.asin(math.sin(_lat) * math.cos(d) + math.cos(_lat) * math.sin(d) * math.cos(brng));
p_lng = (_lng + math.atan2(math.sin(brng) * math.sin(d) * math.cos(_lat), math.cos(d) - math.sin(_lat) * math.sin(p_lat))) * 180 / pi
p_lat = (p_lat * 180) /pi
points.append((p_lat,p_lng))
return polyline.encode(points)
Related
How can I have a what3words style effect on my google map tiles?
For reference, what I'm specifically referring to is how they highlight the square border and draw the line out to the tooltip. I didn't really care about the line but found the mouseover behavior interesting. I also like the hover effect over the various tiles before they're selected. Here it is in action
Here's something I threw together that I'm happy with: https://jsfiddle.net/0zkyx8g9/14/ const HOVERED_CLASS_NAME = "tile-hover-hack" /* * This demo illustrates the coordinate system used to display map tiles in the * API. * * Tiles in Google Maps are numbered from the same origin as that for * pixels. For Google's implementation of the Mercator projection, the origin * tile is always at the northwest corner of the map, with x values increasing * from west to east and y values increasing from north to south. * * Try panning and zooming the map to see how the coordinates change. */ class CoordMapType { tileSize; constructor(tileSize) { this.tileSize = tileSize; } getTile(coord, zoom, ownerDocument) { const div = ownerDocument.createElement("div"); div.id = "block_" + coord.x + "_" + coord.y; div.innerHTML = String(coord); div.style.width = this.tileSize.width + "px"; div.style.height = this.tileSize.height + "px"; div.style.fontSize = "0"; div.style.borderStyle = "solid"; div.style.borderWidth = "1px"; div.style.borderColor = "lightgrey"; div.className = 'tile'; return div; } releaseTile(tile) {} } // must be a factor of 2 (max 256) or swap8 breaks const TILE_SIZE = 32; let map; function initMap() { const chicago = new google.maps.LatLng(41.85, -87.65); map = new google.maps.Map(document.getElementById("map"), { center: chicago, zoom: 3, }); // Insert this overlay map type as the first overlay map type at // position 0. Note that all overlay map types appear on top of // their parent base map. map.overlayMapTypes.insertAt( 0, new CoordMapType(new google.maps.Size(TILE_SIZE, TILE_SIZE)) ); // https://developers.google.com/maps/documentation/javascript/examples/map-coordinates#maps_map_coordinates-javascript map.addListener('mousemove', (event) => { const zoom = map.getZoom(); // console.log('swap 8 on', TILE_SIZE, 'is', swap8(TILE_SIZE)); const scale = swap8(TILE_SIZE) << zoom; const worldCoordinate = project(event.latLng); /* console.log('scale:', scale); console.log('world:', worldCoordinate.x, worldCoordinate.y); */ // x = floor ( 5 * 8 ) / 28 const x = Math.floor((worldCoordinate.x * scale) / TILE_SIZE); const y = Math.floor((worldCoordinate.y * scale) / TILE_SIZE); const tile = document.getElementById('block_' + x + '_' + y); // console.log(x, y); // I have the block! if (previousTile == null) { newTileDetected(tile); } else if (previousTile !== tile) { newTileDetected(tile); } }); } // do all side-effects relating to new tile // 1) de-activate any previous tile styling for active state // 2) active tile styling for new tile let previousTile; const newTileDetected = (tile) => { if (tile == null) { throw Error("New tile detected was null"); } if (previousTile == null) { previousTile = tile; } else { previousTile.className = ""; } tile.className = HOVERED_CLASS_NAME; previousTile = tile; } function swap8(val) { return ((val & 0x1) << 8) | ((val & 0x2) << 6) | ((val & 0x4) << 4) | ((val & 0x8) << 2) | ((val >> 2) & 0x8) | ((val >> 4) & 0x4) | ((val >> 6) & 0x2) | ((val >> 8) & 0x1); } // The mapping between latitude, longitude and pixels is defined by the web // mercator projection. const project = (latLng) => { let 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)) ); } .tile-hover-hack { background-color: rgba(0, 0, 255, 0.5); } This example is what helped a ton for just generally how to handle the mismatch between the information the tiles have available at creation time and the information available in the mouse event.
How to get next latitude and longtitude position acording to distance and first latitude, longtitude point?
I need to find latitude and longtitude according to distance specified. For example I have distance say 50centimeter and one latitude and longtitude point and I want to find what is the next point which is far from 50cm from my first point?
Hey I got the solution and I have implemented this by following calculation from https://www.movable-type.co.uk/scripts/latlong.html and implemented this calculation in c# public static double DegreesToRadians(double degrees) { const double degToRadFactor = Math.PI / 180; return degrees * degToRadFactor; } public static double RadiansToDegrees(double radians) { const double radToDegFactor = 180 / Math.PI; return radians * radToDegFactor; } private double GetBearing(PointLatLng pt1, PointLatLng pt2) { double x = Math.Cos(DegreesToRadians(pt1.Lat)) * Math.Sin(DegreesToRadians(pt2.Lat)) - Math.Sin(DegreesToRadians(pt1.Lat)) * Math.Cos(DegreesToRadians(pt2.Lat)) * Math.Cos(DegreesToRadians(pt2.Lng - pt1.Lng)); double y = Math.Sin(DegreesToRadians(pt2.Lng - pt1.Lng)) * Math.Cos(DegreesToRadians(pt2.Lat)); return (Math.Atan2(y, x) + Math.PI * 2) % (Math.PI * 2); } public static PointLatLng FindPointAtDistanceFrom(PointLatLng startPoint, double initialBearingRadians) { double distanceKilometres = 0.0005; //50cm = 0.0005Km; const double radiusEarthKilometres = 6371.01; var distRatio = distanceKilometres / radiusEarthKilometres; var distRatioSine = Math.Sin(distRatio); var distRatioCosine = Math.Cos(distRatio); var startLatRad = DegreesToRadians(startPoint.Lat); var startLonRad = DegreesToRadians(startPoint.Lng); var startLatCos = Math.Cos(startLatRad); var startLatSin = Math.Sin(startLatRad); var endLatRads = Math.Asin((startLatSin * distRatioCosine) + (startLatCos * distRatioSine * Math.Cos(initialBearingRadians))); var endLonRads = startLonRad + Math.Atan2( Math.Sin(initialBearingRadians) * distRatioSine * startLatCos, distRatioCosine - startLatSin * Math.Sin(endLatRads)); return new PointLatLng { Lat = RadiansToDegrees(endLatRads), Lng = RadiansToDegrees(endLonRads) }; } And Usage of this code is: //Get Angle of point var bearing = GetBearing(polyStartPoint, polyEndPoint); //Get Point from 50cm away for specified point var nextStartPoint = FindPointAtDistanceFrom(polyStartPoint, bearing);
How can I know if a point in the map is currently viewed, based of the DeltaLatitude of the map?
I need to know if a location on the map is currenlt viewed by the user, For example I have a point {latititude:40,longtitude:32} And the view of the map is made from {latitude: 32,longtitude:44, latitudeDelta:0.003,longtitudeDelta:0.005} When the map zooms in and out the only difference in data is the latitudeDelta and the longtitudeDelta Eventually when I'll zoom more and more the point on the map will not be visible anymore Can someone help me build this calculation it will be very helpfull
I can't give you a coded solution but you can: get the diameter from the delta (from 2 angles of the map): const diameter = Math.floor((location.latitudeDelta * 40008000) / 360) Then calculate the distance between 2 positions: function distance(lon1, lat1, lon2, lat2) { const R = 6371 // Radius of the earth in km const dLat = (lat2 - lat1).toRad() // Javascript functions in radians const dLon = (lon2 - lon1).toRad() const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1.toRad()) * Math.cos(lat2.toRad()) * Math.sin(dLon / 2) * Math.sin(dLon / 2) const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) const d = R * c // Distance in km return d } // add this if toRad is undefined if (typeof (Number.prototype.toRad) === 'undefined') { Number.prototype.toRad = function() { return this * Math.PI / 180 } } Then with the diameter and the distance: if ((diameter / 2) > distance) { console.log('The location in in the map') }
Single vs Tiled WMS in Google Maps
I am trying to add a WMS Service to Google Maps JavaScript API with one tile vs multiple tiles. My current code is: states = new google.maps.ImageMapType({ getTileUrl: function (coord, zoom) { // Compose URL for overlay tile var s = Math.pow(2, zoom); var twidth = 256; var theight = 256; var gBl = map.getProjection().fromPointToLatLng(new google.maps.Point(coord.x * twidth / s, (coord.y + 1) * theight / s)); var gTr = map.getProjection().fromPointToLatLng(new google.maps.Point((coord.x + 1) * twidth / s, coord.y * theight / s)); var bottom_x = parseFloat(gBl.lng()) * 20037508.34 / 180; var bottom_y = Math.log(Math.tan((90 + parseFloat(gBl.lat())) * Math.PI / 360)) / (Math.PI / 180); bottom_y = bottom_y * 20037508.34 / 180; var top_x = parseFloat(gTr.lng()) * 20037508.34 / 180; var top_y = Math.log(Math.tan((90 + parseFloat(gTr.lat())) * Math.PI / 360)) / (Math.PI / 180); top_y = top_y * 20037508.34 / 180; var bbox = bottom_x + "," + bottom_y + "," + top_x + "," + top_y; var url = "https://demo.boundlessgeo.com/geoserver/topp/wms?"; url += "&service=WMS"; //WMS service url += "&version=1.1.1"; //WMS version url += "&request=GetMap"; //WMS operation url += "&layers=topp:states"; //WMS layers to draw url += "&styles="; //use default style url += "&format=image/png"; //image format url += "&TRANSPARENT=TRUE"; //only draw areas where we have data url += "&srs=EPSG:3857"; //projection WGS84 url += "&bbox=" + bbox; //set bounding box for tile url += "&width=256"; //tile size used by google url += "&height=256"; return url; //return WMS URL for the tile }, tileSize: new google.maps.Size(256, 256), opacity: 0.85, isPng: true }); Does anyone know how to make a request that ask for one tile vs mutiple tiles?
How to get bounds of a google static map?
How to get bounds in degrees of google static map which has been returned, for example, for following request http://maps.googleapis.com/maps/api/staticmap?center=0.0,0.0&zoom=10&size=640x640&sensor=false As I know, full Earth map is 256x256 image. This means that n vertical pixels contain x degrees, but n horizontal pixels contain 2x degrees. Right? As google says center defines the center of the map, equidistant from all edges of the map. As I understood equidistant in pixels (or in degrees?). And each succeeding zoom level doubles the precision in both horizontal and vertical dimensions. So, I can find delta value of Longitude of map for each zoom value as: dLongitude = (HorizontalMapSizeInPixels / 256 ) * ( 360 / pow(2, zoom) ); Same calculations for Latitude: dLatitude = (VerticalMapSizeInPixels / 256 ) * ( 180 / pow(2, zoom) ); VerticalMapSizeInPixels and HorizontalMapSizeInPixels are parameters of map size in URL. It's good to calculate delta value of Longitude, but for Latitude it is wrong. I cannot find delta value of Latitude, there is some delta error.
As I know, full Earth map is 256x256 image. Yes. This means that n vertical pixels contain x degrees, but n horizontal pixels contain 2x degrees. Right? No. One pixel will represent varying amounts of latitude depending on the latitude. One pixel at the Equator represents less latitude than one pixel near the poles. The corners of the map will depend on center, zoom level and map size, and you'd need to use the Mercator projection to calculate them. If you don't want to load the full API, here's a MercatorProjection object: var MERCATOR_RANGE = 256; function bound(value, opt_min, opt_max) { if (opt_min != null) value = Math.max(value, opt_min); if (opt_max != null) value = Math.min(value, opt_max); return value; } function degreesToRadians(deg) { return deg * (Math.PI / 180); } function radiansToDegrees(rad) { return rad / (Math.PI / 180); } function MercatorProjection() { this.pixelOrigin_ = new google.maps.Point( MERCATOR_RANGE / 2, MERCATOR_RANGE / 2); this.pixelsPerLonDegree_ = MERCATOR_RANGE / 360; this.pixelsPerLonRadian_ = MERCATOR_RANGE / (2 * Math.PI); }; MercatorProjection.prototype.fromLatLngToPoint = function(latLng, opt_point) { var me = this; var point = opt_point || new google.maps.Point(0, 0); var origin = me.pixelOrigin_; point.x = origin.x + latLng.lng() * me.pixelsPerLonDegree_; // NOTE(appleton): 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. var siny = bound(Math.sin(degreesToRadians(latLng.lat())), -0.9999, 0.9999); point.y = origin.y + 0.5 * Math.log((1 + siny) / (1 - siny)) * -me.pixelsPerLonRadian_; return point; }; MercatorProjection.prototype.fromPointToLatLng = function(point) { var me = this; var origin = me.pixelOrigin_; var lng = (point.x - origin.x) / me.pixelsPerLonDegree_; var latRadians = (point.y - origin.y) / -me.pixelsPerLonRadian_; var lat = radiansToDegrees(2 * Math.atan(Math.exp(latRadians)) - Math.PI / 2); return new google.maps.LatLng(lat, lng); }; //pixelCoordinate = worldCoordinate * Math.pow(2,zoomLevel) You can save that to a separate file, for example "MercatorProjection.js", and then include it in your application. <script src="MercatorProjection.js"></script> With the above file loaded, the following function calculates the SW and NE corners of a map of a given size and at a given zoom. function getCorners(center,zoom,mapWidth,mapHeight){ var scale = Math.pow(2,zoom); var centerPx = proj.fromLatLngToPoint(center); var SWPoint = {x: (centerPx.x -(mapWidth/2)/ scale) , y: (centerPx.y + (mapHeight/2)/ scale)}; var SWLatLon = proj.fromPointToLatLng(SWPoint); alert('SW: ' + SWLatLon); var NEPoint = {x: (centerPx.x +(mapWidth/2)/ scale) , y: (centerPx.y - (mapHeight/2)/ scale)}; var NELatLon = proj.fromPointToLatLng(NEPoint); alert(' NE: '+ NELatLon); } and you'd call it like this: var proj = new MercatorProjection(); var G = google.maps; var centerPoint = new G.LatLng(49.141404, -121.960988); var zoom = 10; getCorners(centerPoint,zoom,640,640);
Thanks Marcelo for your answer. It has been quite helpful. In case anyone would be interested, here the Python version of the code (a rough translation of the PHP code, probably not as pythonic as it could be): from __future__ import division import math MERCATOR_RANGE = 256 def bound(value, opt_min, opt_max): if (opt_min != None): value = max(value, opt_min) if (opt_max != None): value = min(value, opt_max) return value def degreesToRadians(deg) : return deg * (math.pi / 180) def radiansToDegrees(rad) : return rad / (math.pi / 180) class G_Point : def __init__(self,x=0, y=0): self.x = x self.y = y class G_LatLng : def __init__(self,lt, ln): self.lat = lt self.lng = ln class MercatorProjection : def __init__(self) : self.pixelOrigin_ = G_Point( MERCATOR_RANGE / 2, MERCATOR_RANGE / 2) self.pixelsPerLonDegree_ = MERCATOR_RANGE / 360 self.pixelsPerLonRadian_ = MERCATOR_RANGE / (2 * math.pi) def fromLatLngToPoint(self, latLng, opt_point=None) : point = opt_point if opt_point is not None else G_Point(0,0) origin = self.pixelOrigin_ point.x = origin.x + latLng.lng * self.pixelsPerLonDegree_ # NOTE(appleton): 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 = bound(math.sin(degreesToRadians(latLng.lat)), -0.9999, 0.9999) point.y = origin.y + 0.5 * math.log((1 + siny) / (1 - siny)) * - self.pixelsPerLonRadian_ return point def fromPointToLatLng(self,point) : origin = self.pixelOrigin_ lng = (point.x - origin.x) / self.pixelsPerLonDegree_ latRadians = (point.y - origin.y) / -self.pixelsPerLonRadian_ lat = radiansToDegrees(2 * math.atan(math.exp(latRadians)) - math.pi / 2) return G_LatLng(lat, lng) #pixelCoordinate = worldCoordinate * pow(2,zoomLevel) def getCorners(center, zoom, mapWidth, mapHeight): scale = 2**zoom proj = MercatorProjection() centerPx = proj.fromLatLngToPoint(center) SWPoint = G_Point(centerPx.x-(mapWidth/2)/scale, centerPx.y+(mapHeight/2)/scale) SWLatLon = proj.fromPointToLatLng(SWPoint) NEPoint = G_Point(centerPx.x+(mapWidth/2)/scale, centerPx.y-(mapHeight/2)/scale) NELatLon = proj.fromPointToLatLng(NEPoint) return { 'N' : NELatLon.lat, 'E' : NELatLon.lng, 'S' : SWLatLon.lat, 'W' : SWLatLon.lng, } Usage : >>> import MercatorProjection >>> centerLat = 49.141404 >>> centerLon = -121.960988 >>> zoom = 10 >>> mapWidth = 640 >>> mapHeight = 640 >>> centerPoint = MercatorProjection.G_LatLng(centerLat, centerLon) >>> corners = MercatorProjection.getCorners(centerPoint, zoom, mapWidth, mapHeight) >>> corners {'E': -65.710988, 'N': 74.11120692972199, 'S': 0.333879313530149, 'W': -178.210988} >>> mapURL = "http://maps.googleapis.com/maps/api/staticmap?center=%f,%f&zoom=%d&size=%dx%d&scale=2&maptype=roadmap&sensor=false"%(centerLat,centerLon,zoom,mapWidth,mapHeight) >>> mapURL http://maps.googleapis.com/maps/api/staticmap?center=49.141404,-121.960988&zoom=10&size=640x640&scale=2&maptype=roadmap&sensor=false'
Simple Python Version Having spent a long time on a similar problem and using this thread for help, here is a simplified Python version of Marcelo (and Jmague)'s code: import math import requests def latLngToPoint(mapWidth, mapHeight, lat, lng): x = (lng + 180) * (mapWidth/360) y = ((1 - math.log(math.tan(lat * math.pi / 180) + 1 / math.cos(lat * math.pi / 180)) / math.pi) / 2) * mapHeight return(x, y) def pointToLatLng(mapWidth, mapHeight, x, y): lng = x / mapWidth * 360 - 180 n = math.pi - 2 * math.pi * y / mapHeight lat = (180 / math.pi * math. atan(0.5 * (math.exp(n) - math.exp(-n)))) return(lat, lng) def getImageBounds(mapWidth, mapHeight, xScale, yScale, lat, lng): centreX, centreY = latLngToPoint(mapWidth, mapHeight, lat, lng) southWestX = centreX - (mapWidth/2)/ xScale southWestY = centreY + (mapHeight/2)/ yScale SWlat, SWlng = pointToLatLng(mapWidth, mapHeight, southWestX, southWestY) northEastX = centreX + (mapWidth/2)/ xScale northEastY = centreY - (mapHeight/2)/ yScale NElat, NElng = pointToLatLng(mapWidth, mapHeight, northEastX, northEastY) return[SWlat, SWlng, NElat, NElng] lat = 37.806716 lng = -122.477702 zoom = 16 picHeight = 640 #Resulting image height in pixels (x2 if scale parameter is set to 2) picWidth = 640 mapHeight = 256 #Original map size - specific to Google Maps mapWidth = 256 xScale = math.pow(2, zoom) / (picWidth/mapWidth) yScale = math.pow(2, zoom) / (picHeight/mapWidth) corners = getImageBounds(mapWidth, mapHeight, xScale, yScale, lat, lng) Here I have used x and y to represent pixel values and lat lng as, Latitude and Longitude. lat, lng, zoom, picHeight and picWidth can all be changed to your specific use case. Changing the scale/ maptype etc will not affect this calculation. I used this code to tile Static Maps images with no gaps/ overlap. If you want to see more of it in use/ how it can work in that sense there is more information on my GitHub.
Here is a line by line translation of Marcelo's code in PHP, which can probably be cleaned up a bit. Works great! Thanks to Marcelo for doing the hard part. define("MERCATOR_RANGE", 256); function degreesToRadians($deg) { return $deg * (M_PI / 180); } function radiansToDegrees($rad) { return $rad / (M_PI / 180); } function bound($value, $opt_min, $opt_max) { if ($opt_min != null) $value = max($value, $opt_min); if ($opt_max != null) $value = min($value, $opt_max); return $value; } class G_Point { public $x,$y; function G_Point($x=0, $y=0){ $this->x = $x; $this->y = $y; } } class G_LatLng { public $lat,$lng; function G_LatLng($lt, $ln){ $this->lat = $lt; $this->lng = $ln; } } class MercatorProjection { private $pixelOrigin_, $pixelsPerLonDegree_, $pixelsPerLonRadian_; function MercatorProjection() { $this->pixelOrigin_ = new G_Point( MERCATOR_RANGE / 2, MERCATOR_RANGE / 2); $this->pixelsPerLonDegree_ = MERCATOR_RANGE / 360; $this->pixelsPerLonRadian_ = MERCATOR_RANGE / (2 * M_PI); } public function fromLatLngToPoint($latLng, $opt_point=null) { $me = $this; $point = $opt_point ? $opt_point : new G_Point(0,0); $origin = $me->pixelOrigin_; $point->x = $origin->x + $latLng->lng * $me->pixelsPerLonDegree_; // NOTE(appleton): 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 = bound(sin(degreesToRadians($latLng->lat)), -0.9999, 0.9999); $point->y = $origin->y + 0.5 * log((1 + $siny) / (1 - $siny)) * -$me->pixelsPerLonRadian_; return $point; } public function fromPointToLatLng($point) { $me = $this; $origin = $me->pixelOrigin_; $lng = ($point->x - $origin->x) / $me->pixelsPerLonDegree_; $latRadians = ($point->y - $origin->y) / -$me->pixelsPerLonRadian_; $lat = radiansToDegrees(2 * atan(exp($latRadians)) - M_PI / 2); return new G_LatLng($lat, $lng); } //pixelCoordinate = worldCoordinate * pow(2,zoomLevel) } function getCorners($center, $zoom, $mapWidth, $mapHeight){ $scale = pow(2, $zoom); $proj = new MercatorProjection(); $centerPx = $proj->fromLatLngToPoint($center); $SWPoint = new G_Point($centerPx->x-($mapWidth/2)/$scale, $centerPx->y+($mapHeight/2)/$scale); $SWLatLon = $proj->fromPointToLatLng($SWPoint); $NEPoint = new G_Point($centerPx->x+($mapWidth/2)/$scale, $centerPx->y-($mapHeight/2)/$scale); $NELatLon = $proj->fromPointToLatLng($NEPoint); return array( 'N' => $NELatLon->lat, 'E' => $NELatLon->lng, 'S' => $SWLatLon->lat, 'W' => $SWLatLon->lng, ); } Usage: $centerLat = 49.141404; $centerLon = -121.960988; $zoom = 10; $mapWidth = 640; $mapHeight = 640; $centerPoint = new G_LatLng($centerLat, $centerLon); $corners = getCorners($centerPoint, $zoom, $mapWidth, $mapHeight); $mapURL = "http://maps.googleapis.com/maps/api/staticmap?center={$centerLat},{$centerLon}&zoom={$zoom}&size={$mapWidth}x{$mapHeight}&scale=2&maptype=roadmap&sensor=false";
Here is translation to Delphi/Pascal with some optimizations to correspond more strict Pascal language and memory management. unit Mercator.Google.Maps; interface uses System.Math; type TG_Point = class(TObject) private Fx: integer; Fy: integer; public property x: integer read Fx write Fx; property y: integer read Fy write Fy; constructor Create(Ax: integer = 0; Ay: integer = 0); end; type TG_LatLng = class(TObject) private FLat: double; FLng: double; public property Lat: double read FLat write FLat; property Lng: double read FLng write FLng; constructor Create(ALat: double; ALng: double); end; type TMercatorProjection = class(TObject) private pixelOrigin_: TG_Point; pixelsPerLonDegree_, pixelsPerLonRadian_: double; function degreesToRadians(deg: double): double; function radiansToDegrees(rad: double): double; function bound(value: double; opt_min: double; opt_max: double): double; public constructor Create; procedure fromLatLngToPoint(latLng: TG_LatLng; var point: TG_Point); procedure fromPointToLatLng(point: TG_point; var latLng: TG_LatLng); procedure getCorners(center: TG_LatLng; zoom: integer; mapWidth: integer; mapHeight: integer; var NELatLon: TG_LatLng; var SWLatLon: TG_LatLng); end; implementation const MERCATOR_RANGE = 256; constructor TG_Point.Create(Ax: Integer = 0; Ay: Integer = 0); begin inherited Create; Fx := Ax; Fy := Ay end; // ************** constructor TG_LatLng.Create(ALat: double; ALng: double); begin inherited Create; FLat := ALat; FLng := ALng end; // ************** constructor TMercatorProjection.Create; begin inherited Create; pixelOrigin_ := TG_Point.Create( Round(MERCATOR_RANGE / 2), Round(MERCATOR_RANGE / 2)); pixelsPerLonDegree_ := MERCATOR_RANGE / 360; pixelsPerLonRadian_ := MERCATOR_RANGE / (2 * PI); end; // Translate degrees to radians function TMercatorProjection.degreesToRadians(deg: double): double; begin Result := deg * (PI / 180); end; // Translate radians to degrees function TMercatorProjection.radiansToDegrees(rad: double): double; begin Result := rad / (PI / 180); end; // keep value insid defined bounds function TMercatorProjection.bound(value: double; opt_min: double; opt_max: double): double; begin if Value < opt_min then Result := opt_min else if Value > opt_max then Result := opt_max else Result := Value; end; procedure TMercatorProjection.fromLatLngToPoint(latLng: TG_LatLng; var point: TG_Point); var siny: double; begin if Assigned(point) then begin point.x := Round(pixelOrigin_.x + latLng.lng * pixelsPerLonDegree_); // NOTE(appleton): 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 := bound(sin(degreesToRadians(latLng.lat)), -0.9999, 0.9999); point.y := Round(pixelOrigin_.y + 0.5 * ln((1 + siny) / (1 - siny)) * -pixelsPerLonRadian_); end; end; procedure TMercatorProjection.fromPointToLatLng(point: TG_point; var latLng: TG_LatLng); var latRadians: double; begin if Assigned(latLng) then begin latLng.lng := (point.x - pixelOrigin_.x) / pixelsPerLonDegree_; latRadians := (point.y - pixelOrigin_.y) / -pixelsPerLonRadian_; latLng.lat := radiansToDegrees(2 * arctan(exp(latRadians)) - PI / 2); end; end; //pixelCoordinate = worldCoordinate * pow(2,zoomLevel) procedure TMercatorProjection.getCorners(center: TG_LatLng; zoom: integer; mapWidth: integer; mapHeight: integer; var NELatLon: TG_LatLng; var SWLatLon: TG_LatLng); var scale: double; centerPx, SWPoint, NEPoint: TG_Point; begin scale := power(2, zoom); centerPx := TG_Point.Create(0, 0); try fromLatLngToPoint(center, centerPx); SWPoint := TG_Point.Create(Round(centerPx.x-(mapWidth/2)/scale), Round(centerPx.y+(mapHeight/2)/scale)); NEPoint := TG_Point.Create(Round(centerPx.x+(mapWidth/2)/scale), Round(centerPx.y-(mapHeight/2)/scale)); try fromPointToLatLng(SWPoint, SWLatLon); fromPointToLatLng(NEPoint, NELatLon); finally SWPoint.Free; NEPoint.Free; end; finally centerPx.Free; end; end; end. Usage example: with TMercatorProjection.Create do try CLatLon := TG_LatLng.Create(Latitude, Longitude); SWLatLon := TG_LatLng.Create(0,0); NELatLon := TG_LatLng.Create(0,0); try getCorners(CLatLon, Zoom, MapWidth, MapHeight, SWLatLon, NELatLon); finally ShowMessage('SWLat='+FloatToStr(SWLatLon.Lat)+' | SWLon='+FloatToStr(SWLatLon.Lng)); ShowMessage('NELat='+FloatToStr(NELatLon.Lat)+' | NELon='+FloatToStr(NELatLon.Lng)); SWLatLon.Free; NELatLon.Free; CLatLon.Free; end; finally Free; end;
For big zoom factors (>=8), where non-uniformity of map scale on y axis can be neglected, there is much easier method, where we just takes into accout 1/cos(latitude) correction for pixels/(degrees of latitude) resolution. Initial resolution for zoom=0 is 256 pixels per 360 degrees both for x and y at 0 lattitude. def get_static_map_bounds(lat, lng, zoom, sx, sy): # lat, lng - center # sx, sy - map size in pixels # 256 pixels - initial map size for zoom factor 0 sz = 256 * 2 ** zoom #resolution in degrees per pixel res_lat = cos(lat * pi / 180.) * 360. / sz res_lng = 360./sz d_lat = res_lat * sy / 2 d_lng = res_lng * sx / 2 return ((lat-d_lat, lng-d_lng), (lat+d_lat, lng+d_lng))