Take snapshot of a specific area under google map javascript - javascript

I am trying to take the snapshot of an area enclosed by a rectangle drawn on the google map. Is it possible to take the snapshot of the area beneath the rectangle? I have searched for answers but couldn't find any helpful info.
Rectangle drawn on the map
I tried to take the snapshot of the area under rectangle using static map API by specifying the map centre, zoom level, image width and image height. Like
https://maps.googleapis.com/maps/api/staticmap?center=CENTER OF THE RECTANGLE&zoom=ZOOM LEVEL OF THE MAP&size=WIDTH AND HEIGHT OF THE RECTANGLE&maptype=satellite&key=API KEY
Here is the code I tried,
var zoom = map.zoom;
var centre = rectangle.getBounds().getCenter(); //rectangle is the shape drawn on the map
var spherical = google.maps.geometry.spherical;
bounds = rectangle.getBounds(); //rectangle is the shape drawn on the map
cor1 = bounds.getNorthEast();
cor2 = bounds.getSouthWest();
cor3 = new google.maps.LatLng(cor2.lat(), cor1.lng());
cor4 = new google.maps.LatLng(cor1.lat(), cor2.lng());
width = spherical.computeDistanceBetween(cor1,cor3);
height = spherical.computeDistanceBetween( cor1, cor4);
Now I downloaded the image using this URL,
"https://maps.googleapis.com/maps/api/staticmap?center=" + centre.lat() + "," + centre.lng() + "&zoom=" + zoom + "&size=" + width + "x" + height + "&maptype=satellite&key=API_KEY"
Original URL : "https://maps.googleapis.com/maps/api/staticmap?center=40.804197008355914,-74.11213619168848&zoom=20&size=26x37&maptype=satellite&key=API_KEY"
Output image I got
My expected output
I got the image, but the image doesn't include the whole area enclosed by the rectangle(It is too small). what I am doing wrong? Is there any other methods to do it?

The main mistake that you have in your code is that the width and height parameters of Static Maps API must be in pixels. Currently you calculate a distance in meters and pass it in Static Maps URLs instead of pixels.
I created an example based on your sample code and was able to create a correct static maps URL. Please have a look at my example. The distanceInPx function is the most important thing. Create the rectangle with drawing manager and click the Show static map link. Also, note that Static Maps API are limited to 640x640 px image size in Standard plan, so you cannot create a link for rectangles that have a bigger size.
function initMap() {
var map = new google.maps.Map(document.getElementById('map'), {
center: {lat: 28.45765, lng: -16.363564},
zoom: 21,
mapTypeId: google.maps.MapTypeId.SATELLITE
});
var drawingManager = new google.maps.drawing.DrawingManager({
drawingMode: google.maps.drawing.OverlayType.RECTANGLE,
drawingControl: true,
drawingControlOptions: {
position: google.maps.ControlPosition.TOP_CENTER,
drawingModes: ['rectangle']
}
});
drawingManager.setMap(map);
google.maps.event.addListener(drawingManager, "rectanglecomplete", function(rectangle){
function distanceInPx(pos1, pos2) {
var p1 = map.getProjection().fromLatLngToPoint(pos1);
var p2 = map.getProjection().fromLatLngToPoint(pos2);
var pixelSize = Math.pow(2, -map.getZoom());
var d = Math.sqrt((p1.x-p2.x)*(p1.x-p2.x) + (p1.y-p2.y)*(p1.y-p2.y))/pixelSize;
return Math.round(d);
}
var zoom = map.zoom;
var centre = rectangle.getBounds().getCenter(); //rectangle is the shape drawn on the map
var spherical = google.maps.geometry.spherical;
bounds = rectangle.getBounds(); //rectangle is the shape drawn on the map
var cor1 = bounds.getNorthEast();
var cor2 = bounds.getSouthWest();
var cor3 = new google.maps.LatLng(cor2.lat(), cor1.lng());
var cor4 = new google.maps.LatLng(cor1.lat(), cor2.lng());
var width = distanceInPx(cor1, cor4);
var height = distanceInPx(cor1, cor3);
var imgUrl = "https://maps.googleapis.com/maps/api/staticmap?center=" +
centre.lat() + "," + centre.lng() + "&zoom=" + zoom +
"&size=" + width + "x" + height + "&maptype=satellite&key=AIzaSyDztlrk_3CnzGHo7CFvLFqE_2bUKEq1JEU";
var aElem = document.getElementById("staticLink");
aElem.setAttribute("href", imgUrl);
aElem.style.display="block";
});
}
#map {
height: 100%;
}
html, body {
height: 95%;
margin: 0;
padding: 0;
}
<div id="map"></div>
<a id="staticLink" href="#" target="_blank" title="Show static map" style="display:none;">Show static map</a>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDztlrk_3CnzGHo7CFvLFqE_2bUKEq1JEU&libraries=drawing&callback=initMap"
async defer></script>
This example is also available at jsbin: http://jsbin.com/humuko/edit?html,output
I hope this helps!

Related

How go we get feature information (like centroid) after the map is rendered and fitToBounds method is called

Following is the actual objective.
-Draw labels on the polygon shapes (like show country name or state name)
-These names to should the middle aligned to the centroid of the polygon
-Avoid overlapping of the labels (before we render a marker we check if this marker will overlap with any marker already rendered. If yes, it should not render the marker)
We wrote a sample and all of the above points are addressed. Following the code.
labels are rendered on the map using marker feature. we generate the HTML and flex classes to center the text to the centroid of the polygon.
to avoid overlapping we make note pixel coordinates the markers created. Each div is a rectangle with minX,minY,maxX,maxY coordinates and we check if rectangles intersect.
var geojson_layer = new L.geoJson(geojson_data, {
onEachFeature: function(feature, layer) {
if (feature.geometry.type.toLowerCase() === "multipolygon") {
var largest = _.maxBy(
feature.geometry.coordinates,
r => r[0].length
);
p = polylabel(largest, 1.0);
} else {
p = polylabel(feature.geometry.coordinates, 1.0);
console.log(p.toString());
}
lon = p[0];
lat = p[1];
if (lat && lon) {
var latLng = L.latLng([lat, lon]);
var point = mymap.latLngToContainerPoint(latLng);
var titleText =
'<svg width="300" height="14"><text x="50%" y="8" fill="black" text-anchor="middle">' +
feature.properties.name +
":more text added" +
"</text></svg>";
var rectArea = new RectangleArea(
point.x - 150,
point.x + 150,
point.y,
point.y + 14
);
var canRender = true;
labelRects.forEach(element => {
if (element.rectanglesIntersect(rectArea)) {
canRender = false;
return;
}
});
if (canRender) {
labelRects.push(rectArea);
var label2 = L.marker([lat, lon], {
icon: L.divIcon({
iconSize: null,
zIndexOffset: 100000,
html:
"<div style='position: relative; left:-50%; display: flex; align-items:center; flex-direction: column; justify-content: center;'>" +
titleText +
"</div>"
})
}).addTo(mymap);
}
}
}
}).addTo(mymap);
All of this works fine except for one problem. In the sample, we set the map center and zoom level like this.
var mymap = L.map("map", { center: [41, -98], zoom: 5 });
In the real application, we don't set zoom level like this. We call fitToBounds method on the layer. This is the problem the above it fails to give us the pixel point if we do not set the zoom level upfront.
Anyone has any idea on how we can get the feature collection and its centroid after the layer is rendered and fitToBounds method is called? Is there any other way of achieving the objective?

Horizontal repeating Google Maps Marker on ImageMapType

<!DOCTYPE html>
<html>
<head>
<title>Image map types</title>
<style>
html, body, #map-canvas {
height: 100%;
margin: 0px;
padding: 0px
}
</style>
<script src="https://maps.googleapis.com/maps/api/js?v=3.exp"></script>
<script>
var moonTypeOptions = {
getTileUrl: function(coord, zoom) {
var bound = Math.pow(2, zoom);
return 'full-out' +
'/' + zoom + '/' + coord.x + '/' +
(bound - coord.y - 1) + '.png';
},
tileSize: new google.maps.Size(256, 256),
maxZoom: 6,
minZoom: 1,
radius: 1738000,
name: 'Moon'
};
var moonMapType = new google.maps.ImageMapType(moonTypeOptions);
function initialize() {
var myLatlng = new google.maps.LatLng(0, 0);
var mapOptions = {
center: myLatlng,
zoom: 1,
streetViewControl: false,
mapTypeControlOptions: {
mapTypeIds: ['moon']
}
};
var map = new google.maps.Map(document.getElementById('map-canvas'),
mapOptions);
map.mapTypes.set('moon', moonMapType);
map.setMapTypeId('moon');
var marker = new google.maps.Marker({
position: myLatlng,
map: map,
title: 'Hello World!'
});
}
google.maps.event.addDomListener(window, 'load', initialize);
</script>
</head>
<body>
<div id="map-canvas"></div>
</body>
</html>
I have above code to render a custom large 16834 * 16834 image of 19 mb which is broken into tiles using gdal2tiles and gdal_translate. In short, I have all the corresponding tiles.
The above code is working perfectly fine rendering image and different zoom levels. However, when I add marker, it is displayed multiple times at lower zoom level. I would like the marker not repeat itself horizontally.
Is there any way to avoid horizontal repeating markers? Currently, I'm using Leaflet.js which doesn't repeat marker horizontally as Google Maps library.
I want to use Google maps because of its stability and popularity.
I'm using Ubuntu 14.04 as OS.
set the optimized-option of the markers to false
For those who has still this problem, have a look at my solution.
1- Set the maps zoom to (2) and add marker positions (lat,long) i.e
var minZoomLevel = 2;
map.setZoom(minZoomLevel);
var bounds = new google.maps.LatLngBounds();
for (var i = 0; i < result.length; i++){
var latlng = new google.maps.LatLng(result[i].Lat, result[i].Lng);
bounds.extend(latlng);
});
2- Attach a event listener on zoom changed i.e
google.maps.event.addListener(map, 'zoom_changed', function() {
if (map.getZoom() < minZoomLevel) map.setZoom(minZoomLevel);
});
3- Attach a center changed listener (This done the trick) i.e
google.maps.event.addListener(map, 'center_changed', function()
{
checkBounds(bounds);
}
function checkBounds(allowedBounds) {
if(allowedBounds.contains(map.getCenter())) {
return;
}
var mapCenter = map.getCenter();
var X = mapCenter.lng();
var Y = mapCenter.lat();
var AmaxX = allowedBounds.getNorthEast().lng();
var AmaxY = allowedBounds.getNorthEast().lat();
var AminX = allowedBounds.getSouthWest().lng();
var AminY = allowedBounds.getSouthWest().lat();
if (X < AminX) {X = AminX;}
if (X > AmaxX) {X = AmaxX;}
if (Y < AminY) {Y = AminY;}
if (Y > AmaxY) {Y = AmaxY;}
map.setCenter(new google.maps.LatLng(Y,X));
}
Every time you change the center, it will check your points and restrict map to certain area . Setting zoom will show only one world tile, and check bound will restrict the horizontal scrolling, thats how your markers will show only one time in map, set zoom according to your condition that fits in !!

Clip Google Maps JS API ImageMapType to a polygon

How can I clip a MapType in Google Maps to an arbitrary polygon. For example, if I have a custom ImageMapType that covers a large area (i.e. all the world), but I want to show it only inside a given polygon (i.e. one country).
Is there a way to clip the ImageMapType to a given polygon, or to implement a custom MapType to achieve this behaviour? It should allow for zooming and panning normally.
The rest of the map should stay the same, and there would be a MapType covering only a specific area. Therefore, it is not possible to simply overlay a polygon to cover the areas outside the polygon to display just what is needed.
Like so:
Server-side clipping is not an option.
I have written the code for an overlay map type that does what you want. Be sure to test in your target browsers. Fiddle
function ClipMapType(polygon, map) {
this.tileSize = new google.maps.Size(256, 256);
this.polygon = polygon;
this.map = map;
}
ClipMapType.prototype.getTile = function(coord, zoom, ownerDocument) {
var map = this.map;
var scale = Math.pow(2, zoom);
if (coord.y < 0 || coord.y >= scale) return ownerDocument.createElement('div');
var tileX = ((coord.x % scale) + scale) % scale;
var tileY = coord.y;
// Your url pattern below
var url = "https://khms0.google.com/kh/v=694&x=" + tileX + "&y=" + tileY + "&z=" + zoom;
var image = new Image();
image.src = url;
var canvas = ownerDocument.createElement('canvas');
canvas.width = this.tileSize.width;
canvas.height = this.tileSize.height;
var context = canvas.getContext('2d');
var xdif = coord.x * this.tileSize.width;
var ydif = coord.y * this.tileSize.height;
var ring = this.polygon.getArray()[0];
var points = ring.getArray().map(function(x) {
var worldPoint = map.getProjection().fromLatLngToPoint(x);
return new google.maps.Point((worldPoint.x) * scale - xdif, (worldPoint.y) * scale - ydif);
});
image.onload = function() {
context.beginPath();
context.moveTo(points[0].x, points[0].y);
var count = points.length;
for (var i = 0; i < count; i++) {
context.lineTo(points[i].x, points[i].y);
}
context.lineTo(points[count - 1].x, points[count - 1].y);
context.clip();
context.drawImage(image, 0, 0);
context.closePath();
};
return canvas;
};
function initMap() {
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 4,
center: {
lat: 15,
lng: 15
}
});
var polygon = new google.maps.Data.Polygon([
[{
lat: 0,
lng: 0
}, {
lat: 30,
lng: 30
}, {
lat: 0,
lng: 30
}]
]);
var mapType = new ClipMapType(polygon, map);
map.overlayMapTypes.insertAt(0, mapType);
}
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
#map {
height: 100%;
}
<div id="map"></div>
<script async defer src="https://maps.googleapis.com/maps/api/js?callback=initMap">
</script>
How it works
Basically ClipMapType class is a MapType interface. getTile method of this interface is called with tile coordinates and zoom level to get tile for every tile. ClipMapType creates a canvas element to act as a tile and draws the tile image clipped to inside of the polygon. If performance is important, it can be optimized to work faster.
Disclaimer
Usage of Google tile servers by hacking the URL, probably violates Google Maps Terms of Service. I used it for demonstration and don't recommend using it in production. My answer is an attempt to give you an insight for you to create your own solution.
Do you require Google Maps perse? I know Openlayers 3 provides better support for this kind of stuff. For example, take a look at this.
If you really must use Google Maps, I suggest implementing your own MapType and generate the tiles needed to cover your polygon area yourself using MapTiler. (MapTiler also generates an example Google Maps implementation for you, so that shouldn't be too hard.)
You can use the canvas.toDataURI() option in HTML5 to obtain the url that is required for getTileUrl() of ImageMapType.
getTileUrl: function(coord, zoom) {
var normalizedCoord = getNormalizedCoord(coord, zoom);
if (!normalizedCoord) {
return null;
}
var bound = Math.pow(2, zoom);
// reset and clip the preloaded image in a hidden canvas to your required height and width
clippedImage = canvas.toDataURL();
return clippedImage;
}
To set and resize the image to correct dimension, use canvas.drawImage()
To clip the image from canvas to any non-rectangular dimension, use the canvas clip()
Sample code for canvas clipping.
I see that you can't use normal masking strategies because you need to be able to see the lower layer. May I suggest SVG's more complete clipping suite? See here.
The browser compatibility is good but not great, but you can absolutely accomplish what you're trying here (unless you need to pan/zoom the Map, then you're screwed until Maps implements such a thing).
You could use an svg clippath, together with the foreignobject svg tag to put a html document within the svg then clip it to the desired shape like this code taken from codepen.io/yoksel/pen/oggRwR:
#import url(http://fonts.googleapis.com/css?family=Arvo:700);
.svg {
display: block;
width: 853px;
height: 480px;
margin: 2em auto;
}
text {
font: bold 5.3em/1 Arvo, Arial sans-serif;
}
<svg class="svg">
<clippath id="cp-circle">
<circle r="180" cx="50%" cy="42%"></circle>
<text
text-anchor="middle"
x="50%"
y="98%"
>Soldier Of Fortune</text>
</clippath>
<g clip-path="url(#cp-circle)">
<foreignObject width="853" x="0"
y="0" height="480">
<body xmlns="http://www.w3.org/1999/xhtml">
<iframe width="853" height="480" src="//www.youtube.com/embed/RKrNdxiBW3Y" frameborder="0" allowfullscreen></iframe>
</body>
</foreignObject>
</g>
</svg>
http://codepen.io/yoksel/pen/oggRwR
You could place a DIV above your map, with absolute positioning and high z-index. then, apply a polygon mask to that DIV like this: -webkit-clip-path: polygon(0 0, 0 100%, 100% 0);

TileMill MapBox map pans to different world without any markers

I have a map I exported from tilemill, made a mapbox map and threw some points on it. The view starts off looking at the US, with a marker somewhere in the middle. If I pan left until the next time I see the US, the markers are gone. Here's the code minus the geoJson data.
var map = L.mapbox.map('map', 'natecraft1.xdan61or').setView([-102, 39], 4);
map.markerLayer.on('layeradd', function(e) {
var marker = e.layer;
var feature = marker.feature;
var image = feature.properties.images
// var img = images
// Create custom popup content
var popupContent = '<img class="pics" src="' + image + '" />'
marker.bindPopup(popupContent,{
closeButton: false,
minWidth: 320,
offset: [180, 20]
});
});
map.markerLayer.setGeoJSON(geoJson);
map.markerLayer.on('click', function(e) {
e.layer.openPopup();
var lat = e.layer.getLatLng().lat;
var lng = e.layer.getLatLng().lng;
map.panTo([lat+5, lng+5], 2);
});
map.markerLayer.on('', function(e) {
e.layer.closePopup();
});
Your tilelayer is wrapping around the globe for coordinates outside of the (-180, 180) range. Best option is to set the maxBounds option so users don't pan outside of that map and just get bounced back.
var map = L.mapbox.map('map', 'examples.map-9ijuk24y').setView([0,0], 2);
var layer = L.mapbox.tileLayer('examples.map-9ijuk24y', {
noWrap: true,
minZoom: 3
}).addTo(map);
map.options.maxBounds = map.getBounds();
Here's a live demo of what that would look like

Move OpenLayers.Popup up to 10 pixel and left 15 pixel?

Good afternoon, I have a popup that is in the map and I want only move 10 pixels high and 15 pixels to the left, the problem is that when you change its position in latitude and longitude, is in another position completely and when I zoom away from the marker, what I want is just to move to the new position regardless of the zoom, always remains above the marker.
var size = new OpenLayers.Size(21,25);
var offset = new OpenLayers.Pixel(-(size.w/2), -size.h);
var icon = new OpenLayers.Icon('/openlayers/img/marker.png',size,offset);
var lonlat = new OpenLayers.LonLat(long,lat);
var proj_1 = new OpenLayers.Projection("EPSG:4326");
var proj_2 = new OpenLayers.Projection("EPSG:900913");
var EPSG = lonlat.transform(proj_1,proj_2);
var marker = new OpenLayers.Marker(EPSG, icon);
markers.addMarker(marker);
marker.events.register("click", marker, function(e){ // on click popup
var popup = new OpenLayers.Popup.FramedCloud(id,
marker.lonlat,
new OpenLayers.Size(200,200),
'<div class="popup">info example</div>',
null,true);
map.addPopup(popup);
});
var labelepopup = new OpenLayers.Popup(null,
EPSG,
new OpenLayers.Size(37,13),
'<p style="font-size: 8.5px;">always info</p>'
);
map.addPopup(labelepopup);
The popup on the marker as labelepopup appear, all I want is to accommodate the labelepopup above the marker.
You can use getPixelFromLonLat map function to get the exact pixel from a latitude and a longitude and then use the Pixel object returned to add or remove the desired pixels. Then you can change popup position using moveTo popup function that requires a Pixel object.
Something like this:
var pixel = map.getPixelFromLonLat(popup.lonlat); //or use another OpenLayers.LonLat object
pixel.x += DESIRED_X_AMOUNT;
pixel.y += DESIRED_Y_AMOUNT;
popup.moveTo(pixel);

Categories

Resources