A little background. This map draws polygons on the quarter of each minute. They are labelled as such. That seems to work fine. I am adding a second polygon that is editable and draggable to the map which the user is permitted to select quarter minutes by expanding the handles on the gridSelectBox and then click outside of the selected area. I will be written the quarter minute to a database.
That works but with issues.
Sometimes only every other column or row (Long or Lat) is selected though it is clearly in the selected area. I can only assume it is some sort of rounding area. I did notice that what should be a perfect degree X degree vertices 34.00 X -84.00 appears as 34.00000000000001 X -84.00. Could that be the problem, and if so, how do I correct for it?
fiddle
<html>
<head>
<script type ="text/javascript"
src ="https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=geometry&sensor=false"></script>
<title>Find your Qtr minute locator</title>
</head>
<body style="height:100%;margin:0">
<!-- Declare the div, make it take up the full document body -->
<div id="map-canvas" style="width:100%; height: 100%;"></div>
<script type="text/javascript">
var map;
var qtrArray = [];
var Startlatlng = "";
var llOffset =((1/60)/4);
var drawGridSelectBox = false;
var firstRun = true;
var drawGridBox = false;
var gridOverBox = new google.maps.Polygon();
var gridSelectBox = new google.maps.Polygon();
var gridline;
var polylinesquare;
var latPolylines = [];
var latLabels = [];
var latMapLabel;
var lngPolylines = [];
var lngLabels = [];
var lngMapLabel;
var bounds = new google.maps.LatLngBounds();
function initialize() {
map = new google.maps.Map(document.getElementById("map-canvas"), {
center: new google.maps.LatLng(34.0, -84.0),
zoom: 16,
streetViewControl: true,
mapTypeId: google.maps.MapTypeId.ROADMAP,
scaleControl: true
});
google.maps.event.addListener(map, "click", function (event) {
if (!google.maps.geometry.poly.containsLocation(event.latLng, gridSelectBox)) {
map.setZoom(16);
createGridSelectBox(event.latLng);
ClearGrid();
for(var x=0;x<latPolylines.length;++x){
for(var y=0;y<lngPolylines.length-1;++y){
var latLng=new google.maps.LatLng(latPolylines[x].getPath().getAt(0).lat(),
lngPolylines[y].getPath().getAt(0).lng());
if ((google.maps.geometry.poly.containsLocation(latLng, gridSelectBox))&&(!firstRun))
{
drawGridBox = true;
createGridBox(latLng);
}
}
}
firstRun = false;
map.panTo(event.latLng);
drawGridBox = false;
}});
DrawGridOn();
google.maps.event.addListener(map, "idle", function () {
createGridLines(map.getBounds());
});
}
google.maps.event.addDomListener(window, "load", initialize);
function DrawGridOn() {
drawGridSelectBox = true;
}
function DrawGridOff() {
drawGridSelectBox = false;
}
function createGridLines(bounds) {
for (var i = 0; i < latPolylines.length; i++) {
latPolylines[i].setMap(null);
}
latPolylines = [];
for (var j = 0; j < lngPolylines.length; j++) {
lngPolylines[j].setMap(null);
}
lngPolylines = [];
if (map.getZoom() < 14) return;
var north = bounds.getNorthEast().lat();
var east = bounds.getNorthEast().lng();
var south = bounds.getSouthWest().lat();
var west = bounds.getSouthWest().lng();
// define the size of the grid
var topLat = Math.ceil(north / llOffset) * llOffset;
var rightLong = Math.ceil(east / llOffset) * llOffset;
var bottomLat = Math.floor(south / llOffset) * llOffset;
var leftLong = Math.floor(west / llOffset) * llOffset;
for (var latitude = bottomLat; latitude <= topLat; latitude += llOffset) latPolylines.push(new google.maps.Polyline({
path: [
new google.maps.LatLng(latitude, leftLong), new google.maps.LatLng(latitude, rightLong)],
map: map,
geodesic: true,
strokeColor: "#0000FF",
strokeOpacity: 0.5,
strokeWeight: 1
}));
for (var longitude = leftLong; longitude <= rightLong; longitude += llOffset) lngPolylines.push(new google.maps.Polyline({
path: [
new google.maps.LatLng(topLat, longitude), new google.maps.LatLng(bottomLat, longitude)],
map: map,
geodesic: true,
strokeColor: "#0000FF",
strokeOpacity: 0.5,
strokeWeight: 1
}));
if (map.getZoom() < 15) {
for (var i = 0; i < lngLabels.length; i++) {
lngLabels[i].setMap(null);
}
lngLabels = [];
return;
} // set lngLabels to null
for(var x=0;x<latPolylines.length;++x){
for(var y=0;y<lngPolylines.length-1;++y){
var latLng=new google.maps.LatLng(latPolylines[x].getPath().getAt(0).lat(),
lngPolylines[y].getPath().getAt(0).lng());
var qtrLatLng = ddToQM(latLng.lat(), latLng.lng());
lngLabels.push(new google.maps.Marker({
map:map,
position:latLng,
icon:{ url:"https://chart.googleapis.com/chart?"
+"chst=d_bubble_text_small&chld=bb|"
+ latLng
+"|FFFFFF|000000",
anchor:new google.maps.Point(0,42)
}
}));
}
}
} // end createGridLines
function createGridSelectBox(point) {
// Square limits
var smPoint = point;
var babyOffset = (llOffset/2);
var bottomLeftLat = (Math.floor(point.lat() / llOffset) * llOffset)-babyOffset;
var bottomLeftLong = (Math.floor(point.lng() / llOffset) * llOffset) - babyOffset;
var gridLineSquare = [
new google.maps.LatLng(bottomLeftLat, bottomLeftLong), //lwr left
new google.maps.LatLng(bottomLeftLat, bottomLeftLong + llOffset), //lwr right
new google.maps.LatLng(bottomLeftLat + llOffset, bottomLeftLong + llOffset), //upr right
new google.maps.LatLng(bottomLeftLat + llOffset, bottomLeftLong), //upr left
new google.maps.LatLng(bottomLeftLat, bottomLeftLong)]; //lwr left
if (drawGridSelectBox == true) {
gridSelectBox = new google.maps.Polygon({
path: gridLineSquare,
draggable:true,
geodesic:true,
editable :true,
fillColor: "#FF0000",
fillOpacity: 0.35,
strokeColor: "#CC0099",
strokeOpacity: 0.5,
strokeWeight: 2
});
gridSelectBox.setMap(map);
drawGridSelectBox = false;
}
}
function ClearGrid() {
if (qtrArray) {
for (i in qtrArray) {
qtrArray[i].setMap(null);
}
}
qtrArray=[];
}
function createGridBox(point) {
// Square limits
var smPoint = point;
var bottomLeftLat = Math.floor(point.lat() / llOffset) * llOffset;
var bottomLeftLong = Math.floor(point.lng() / llOffset) * llOffset;
var gridLineSquare = [
new google.maps.LatLng(bottomLeftLat, bottomLeftLong), //lwr left
new google.maps.LatLng(bottomLeftLat, bottomLeftLong + llOffset), //lwr right
new google.maps.LatLng(bottomLeftLat + llOffset, bottomLeftLong + llOffset), //upr right
new google.maps.LatLng(bottomLeftLat + llOffset, bottomLeftLong), //upr left
new google.maps.LatLng(bottomLeftLat, bottomLeftLong)]; //lwr left
if (drawGridBox == true) {
gridOverBox = new google.maps.Polygon({
path: gridLineSquare,
draggable:false,
geodesic:true,
editable :false,
fillColor: "#EAED00",
fillOpacity: 0.35,
strokeColor: "#CC0099",
strokeOpacity: 0.5,
strokeWeight: 2
});
gridOverBox.setMap(map);
qtrArray.push(gridOverBox);
}
}
function ddToQM(alat, alng) {
var latResult, lngResult, dmsResult;
alat = parseFloat(alat);
alng = parseFloat(alng);
latResult = (alat >= 0)? "" : "";
latResult += getDms(alat);
lngResult = (alng >= 0)? "" : "";
lngResult += getDms(alng);
dmsResult = latResult + lngResult;
// Return the resultant string.
return dmsResult;
}
function getDms(val) {
// Required variables
var valDeg, valMin, valSec, interimResult;
var qtrMin;
val = Math.abs(val);
// ---- Degrees ----
valDeg = Math.floor(val);
valMin = Math.floor((val - valDeg) * 60);
valSec = Math.round((val - valDeg - valMin / 60) * 3600 * 1000) / 1000;
if (valSec == 60){
valMin +=1;
valSec = 0;
}
if (valMin == 60){
valMin +=1;
valSec = 0;
}
interimResult = valDeg+"";
if (valMin<10){
valMin = "0"+valMin;
}
interimResult += valMin + "";
switch(valSec){
case 0 : qtrMin = "A";
break;
case 15 : qtrMin = "B";
break;
case 30 : qtrMin = "C";
break;
case 45 : qtrMin = "D";
break;
}
interimResult += qtrMin;
return interimResult;
}
</script>
</body>
</html>
After weeks of ripping my script apart, I finally found it. The issue was the LLOffset constant. The grid boxes are to be 1/4th minute X 1/4th minute. Thus the constant of
var llOffset = (1/60)/4);
or 1 degree/60 minutes / 4
I used the fractional computation and let JavaScript decide the format.
JS determined that 1/60)/4 = 0.004166666666666667 or 18 significant digits yet when it computes a LatLng, it uses only 14 significant digits. Normally the difference would not matter but every so often, that .000000000000006667 was enough to push my polygon anchor into the next grid.
Simply changing llOffset to 0.00416666666667 (the same as google maps LatLng, corrected the problem.
This may be useful information when close tolerance is desired.
Related
I am using google maps API for searching houses in different places in a city. All the houses will be having markers and user will draw multiple polygons around the markers based on his interest.
I need to check what all the locations user selected and show only those markers in maps. With my code, I am able to get it done, but struck with some issue
Once user draw first polygon, I am storing that in a variable and then merging it with the coordinates of second polygon and repeating the same for all the polygons. So, all the polygons coordinates are in a single object. Whenever I draw multiple polygons all of them are getting connected with a polyline because I am saving them in one object and sending that to mapOptions.
How to get rid of this issue?? Following is the code, I am using when user draws a polygon. I want all the polygons to be independent to each-other
function drawFreeHand(){
//the polygon
poly = new google.maps.Polyline({map:map,clickable:false});
//move-listener
var move = google.maps.event.addListener(map,'mousemove',function(e){
poly.getPath().push(e.latLng);
});
//mouseup-listener
google.maps.event.addListenerOnce(map,'mouseup',function(e){
google.maps.event.removeListener(move);
var path = poly.getPath();
poly.setMap(null);
var theArrayofLatLng = path.j;
var ArrayforPolygontoUse = GDouglasPeucker(theArrayofLatLng,50);
multi_poly = myFunction1(ArrayforPolygontoUse)
console.log(multi_poly);
var polyOptions = {
map: map,
fillColor: '#0099FF',
fillOpacity: 0.7,
strokeColor: '#AA2143',
strokeWeight: 2,
clickable: false,
zIndex: 1,
path:multi_poly,
editable: false
}
poly = new google.maps.Polygon(polyOptions);
});
}
function myFunction1(myVar) {
if(ArrayforPolygontoUse != undefined) {
str1 = ArrayforPolygontoUse;
}
else {
var str1 = [];
}
return ArrayforPolygontoUse = $.merge(str1,myVar);
}
A polygon can take an array of arrays of google.maps.LatLng objects. That is what you want to do if you want to keep the paths separate.
function drawFreeHand(){
//the polygon
poly = new google.maps.Polyline({map:map,clickable:false});
//move-listener
var move = google.maps.event.addListener(map,'mousemove',function(e){
poly.getPath().push(e.latLng);
});
//mouseup-listener
google.maps.event.addListenerOnce(map,'mouseup',function(e){
google.maps.event.removeListener(move);
var path = poly.getPath();
poly.setMap(null);
var theArrayofLatLng = path.getArray();
var ArrayforPolygontoUse = GDouglasPeucker(theArrayofLatLng,50);
if (poly && poly.getPaths) {
// if already has one or more paths, get the existing paths
multi_poly = poly.getPaths();
multi_poly_path.push(ArrayforPolygontoUse);
} else {
// first path
multi_poly = ArrayforPolygontoUse;
}
var polyOptions = {
map: map,
fillColor: '#0099FF',
fillOpacity: 0.7,
strokeColor: '#AA2143',
strokeWeight: 2,
clickable: false,
zIndex: 1,
paths:multi_poly,
editable: false
}
poly = new google.maps.Polygon(polyOptions);
});
}
proof of concept fiddle
code snippet:
var geocoder;
var map;
var ArrayforPolygontoUse = [];
function initialize() {
map = new google.maps.Map(
document.getElementById("map_canvas"), {
center: new google.maps.LatLng(37.4419, -122.1419),
zoom: 13,
mapTypeId: google.maps.MapTypeId.ROADMAP
});
google.maps.event.addListener(map, 'rightclick', drawFreeHand);
}
google.maps.event.addDomListener(window, "load", initialize);
function drawFreeHand() {
//the polygon
poly = new google.maps.Polyline({
map: map,
clickable: false
});
//move-listener
var move = google.maps.event.addListener(map, 'mousemove', function(e) {
poly.getPath().push(e.latLng);
});
//mouseup-listener
google.maps.event.addListenerOnce(map, 'mouseup', function(e) {
google.maps.event.removeListener(move);
var path = poly.getPath();
poly.setMap(null);
var theArrayofLatLng = path.getArray();
var ArrayforPolygontoUse = GDouglasPeucker(theArrayofLatLng, 50);
if (poly && poly.getPaths) {
multi_poly = poly.getPaths();
multi_poly_path.push(ArrayforPolygontoUse);
} else {
multi_poly = ArrayforPolygontoUse;
}
// multi_poly = myFunction1(ArrayforPolygontoUse)
// console.log(multi_poly);
var polyOptions = {
map: map,
fillColor: '#0099FF',
fillOpacity: 0.7,
strokeColor: '#AA2143',
strokeWeight: 2,
clickable: false,
zIndex: 1,
paths: multi_poly,
editable: false
}
poly = new google.maps.Polygon(polyOptions);
});
}
// from http://stackoverflow.com/questions/16121236/smoothing-gps-tracked-route-coordinates
/* Stack-based Douglas Peucker line simplification routine
returned is a reduced google.maps.LatLng array
After code by Dr. Gary J. Robinson,
Environmental Systems Science Centre,
University of Reading, Reading, UK
*/
function GDouglasPeucker(source, kink)
/* source[] Input coordinates in google.maps.LatLngs */
/* kink in metres, kinks above this depth kept */
/* kink depth is the height of the triangle abc where a-b and b-c are two consecutive line segments */
{
var n_source, n_stack, n_dest, start, end, i, sig;
var dev_sqr, max_dev_sqr, band_sqr;
var x12, y12, d12, x13, y13, d13, x23, y23, d23;
var F = ((Math.PI / 180.0) * 0.5);
var index = new Array(); /* aray of indexes of source points to include in the reduced line */
var sig_start = new Array(); /* indices of start & end of working section */
var sig_end = new Array();
/* check for simple cases */
if (source.length < 3) return (source); /* one or two points */
/* more complex case. initialize stack */
n_source = source.length;
band_sqr = kink * 360.0 / (2.0 * Math.PI * 6378137.0); /* Now in degrees */
band_sqr *= band_sqr;
n_dest = 0;
sig_start[0] = 0;
sig_end[0] = n_source - 1;
n_stack = 1;
/* while the stack is not empty ... */
while (n_stack > 0) {
/* ... pop the top-most entries off the stacks */
start = sig_start[n_stack - 1];
end = sig_end[n_stack - 1];
n_stack--;
if ((end - start) > 1) { /* any intermediate points ? */
/* ... yes, so find most deviant intermediate point to
either side of line joining start & end points */
x12 = (source[end].lng() - source[start].lng());
y12 = (source[end].lat() - source[start].lat());
if (Math.abs(x12) > 180.0) x12 = 360.0 - Math.abs(x12);
x12 *= Math.cos(F * (source[end].lat() + source[start].lat())); /* use avg lat to reduce lng */
d12 = (x12 * x12) + (y12 * y12);
for (i = start + 1, sig = start, max_dev_sqr = -1.0; i < end; i++) {
x13 = (source[i].lng() - source[start].lng());
y13 = (source[i].lat() - source[start].lat());
if (Math.abs(x13) > 180.0) x13 = 360.0 - Math.abs(x13);
x13 *= Math.cos(F * (source[i].lat() + source[start].lat()));
d13 = (x13 * x13) + (y13 * y13);
x23 = (source[i].lng() - source[end].lng());
y23 = (source[i].lat() - source[end].lat());
if (Math.abs(x23) > 180.0) x23 = 360.0 - Math.abs(x23);
x23 *= Math.cos(F * (source[i].lat() + source[end].lat()));
d23 = (x23 * x23) + (y23 * y23);
if (d13 >= (d12 + d23)) dev_sqr = d23;
else if (d23 >= (d12 + d13)) dev_sqr = d13;
else dev_sqr = (x13 * y12 - y13 * x12) * (x13 * y12 - y13 * x12) / d12; // solve triangle
if (dev_sqr > max_dev_sqr) {
sig = i;
max_dev_sqr = dev_sqr;
}
}
if (max_dev_sqr < band_sqr) { /* is there a sig. intermediate point ? */
/* ... no, so transfer current start point */
index[n_dest] = start;
n_dest++;
} else {
/* ... yes, so push two sub-sections on stack for further processing */
n_stack++;
sig_start[n_stack - 1] = sig;
sig_end[n_stack - 1] = end;
n_stack++;
sig_start[n_stack - 1] = start;
sig_end[n_stack - 1] = sig;
}
} else {
/* ... no intermediate points, so transfer current start point */
index[n_dest] = start;
n_dest++;
}
}
/* transfer last point */
index[n_dest] = n_source - 1;
n_dest++;
/* make return array */
var r = new Array();
for (var i = 0; i < n_dest; i++)
r.push(source[index[i]]);
return r;
}
html,
body,
#map_canvas {
height: 100%;
width: 100%;
margin: 0px;
padding: 0px
}
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCkUOdZ5y7hMm0yrcCQoCvLwzdM6M8s5qk"></script>
<div id="map_canvas" style="border: 2px solid #3872ac;"></div>
I want to do static labels (under my line and parallel my line) for polyline in leaflet.
For example:
http://jsfiddle.net/jypp24oq/5/
firstpolyline.bindLabel('Even polylines can have labels.', { noHide: true });
But 'noHide' parametr not work for polylines? and also I want to do label parallel my line.
How I can do it?
You wont be able to just bind a label to a polyline. It will need additional coordinates based on the line you will want to bind it to.
Checkout the example on fiddle that includes a helper function called "bindLabelEx" that handles the polyline labeling for you:
https://jsfiddle.net/jZv7W/158/
$(document).ready(function() {
L.Polyline.include({
bindLabelEx: function (content, options) {
if (!this.label || this.label.options !== options) {
this.label = new L.Label(options, this);
}
var latlngs = this.getLatLngs();
var nPoint = latlngs.length;
var lats = [];
var lngs = [];
for(var i = 0; i < nPoint; i++) {
lats.push(latlngs[i].lat);
lngs.push(latlngs[i].lng);
}
var minLat = Math.min.apply(null, lats);
var maxLat = Math.max.apply(null, lats);
var minLng = Math.min.apply(null, lngs);
var maxLng = Math.max.apply(null, lngs);
var pointM = {
lat: (minLat + maxLat) / 2,
lng: (minLng + maxLng) / 2
};
this.label.setContent(content);
this._showLabelAdded = true;
this._showLabel({
latlng: pointM
});
}
});
L.RotatedMarker = L.Marker.extend({
_setPos: function(pos) {
L.Marker.prototype._setPos.call(this, pos);
if (L.DomUtil.TRANSFORM) {
// use the CSS transform rule if available
this._icon.style[L.DomUtil.TRANSFORM] += ' rotate(' + this.options.angle + 'deg)';
} else if (L.Browser.ie) {
// fallback for IE6, IE7, IE8
var rad = this.options.angle * L.LatLng.DEG_TO_RAD,
costheta = Math.cos(rad),
sintheta = Math.sin(rad);
this._icon.style.filter += ' progid:DXImageTransform.Microsoft.Matrix(sizingMethod=\'auto expand\', M11=' +
costheta + ', M12=' + (-sintheta) + ', M21=' + sintheta + ', M22=' + costheta + ')';
}
}
});
L.rotatedMarker = function(pos, options) {
return new L.RotatedMarker(pos, options);
};
//example user location
var userLocation = new L.LatLng(28.735, 77.524);
var map = L.map('map').setView(userLocation, 10);
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © CloudMade'
}).addTo(map);
var pointA = new L.LatLng(28.635308, 77.22496);
var pointB = new L.LatLng(28.984461, 77.70641);
//var pointC = new L.LatLng(29.03, 77.20);
//var pointD = new L.LatLng(28.52, 77.45);
var pointM = new L.LatLng( (pointA.lat + pointB.lat) / 2, (pointA.lng + pointB.lng) / 2);
var pointList = [pointA, pointB];
//var pointList = [pointA, pointB, pointC, pointD];
var firstpolyline = new L.Polyline(pointList, {
color: 'red',
weight: 3,
opacity: 0.5,
smoothFactor: 1
});
firstpolyline.addTo(map).bindLabelEx('Even polylines can have labels.', { noHide: true, showLabelAdded: true });
var angle = Math.atan( (pointB.lat - pointA.lat) / (pointB.lng - pointA.lng) );
angle *= 180 / Math.PI + 5;
var marker = L.rotatedMarker(pointM, {
icon: L.divIcon({
className: 'label',
html: 'Do you want me to do?',
iconSize: [160, 40]
}),
angle: -angle
//draggable: true
});
marker.addTo(map);
/*
var ll = marker.getLatLng();
marker.options.angle = -45 * (180 / Math.PI);
market.setLatLng(ll);
*/
});
You can create labels attached to iconless markers located on the middle point of your polylines by calculating and setting each middle point position with help of following formula,
Math.round(arrPolylinePoints.length / 2) - 1;
I'm trying to put together a Image map with a custom projection. I borrowed code from the following 3 URL's:
https://developers.google.com/maps/documentation/javascript/examples/map-coordinates
https://developers.google.com/maps/documentation/javascript/examples/maptype-image
https://developers.google.com/maps/documentation/javascript/examples/map-projection-simple
I added code to loop through all Lat and Lng values in 10 degree increments, but only some markers actually show.
If I remove the line:
mapType.projection = new MercatorProjection();
then the markers all show as expected. That means I can't use my custom projection, unless there is a better way.
All the Javascript code is below and should show you the moon with markers.
Thanks.
<!DOCTYPE html>
<html>
<head>
<title>Showing pixel and tile coordinates</title>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<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 map;
var TILE_SIZE = 256;
var chicago = new google.maps.LatLng(41.850033,-87.6500523);
function modulo(n, d) {
var result = (n % d + d) % d;
return result;
}
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);
}
/** #constructor */
function MercatorProjection() {
this.pixelOrigin_ = new google.maps.Point(TILE_SIZE / 2, TILE_SIZE / 2);
this.pixelsPerLonDegree_ = TILE_SIZE / 360;
this.pixelsPerLonRadian_ = TILE_SIZE / (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_;
// 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);
};
// Normalizes the coords that tiles repeat across the x axis (horizontally)
// like the standard Google map tiles.
function getNormalizedCoord(coord, zoom) {
var y = coord.y;
var x = coord.x;
// tile range in one direction range is dependent on zoom level
// 0 = 1 tile, 1 = 2 tiles, 2 = 4 tiles, 3 = 8 tiles, etc
var numTiles = 1 << zoom;
// don't repeat across y-axis (vertically)
if (y < 0 || y >= numTiles) {
return null;
}
// repeat across x-axis
if (x < 0 || x >= numTiles) {
x = modulo(x, numTiles);
}
return {
x: x,
y: y
};
}
function createInfoWindowContent() {
var numTiles = 1 << map.getZoom();
var projection = new MercatorProjection();
var worldCoordinate = projection.fromLatLngToPoint(chicago);
var pixelCoordinate = new google.maps.Point(
worldCoordinate.x * numTiles,
worldCoordinate.y * numTiles);
var tileCoordinate = new google.maps.Point(
Math.floor(pixelCoordinate.x / TILE_SIZE),
Math.floor(pixelCoordinate.y / TILE_SIZE));
return [
'Chicago, IL',
'LatLng: ' + chicago.lat() + ' , ' + chicago.lng(),
'World Coordinate: ' + worldCoordinate.x + ' , ' +
worldCoordinate.y,
'Pixel Coordinate: ' + Math.floor(pixelCoordinate.x) + ' , ' +
Math.floor(pixelCoordinate.y),
'Tile Coordinate: ' + tileCoordinate.x + ' , ' +
tileCoordinate.y + ' at Zoom Level: ' + map.getZoom()
].join('<br>');
}
function initialize() {
var mapType = new google.maps.ImageMapType({
getTileUrl: function(coord, zoom) {
var normalizedCoord = getNormalizedCoord(coord, zoom);
if (!normalizedCoord) {
return null;
}
var bound = Math.pow(2, zoom);
return 'http://mw1.google.com/mw-planetary/lunar/lunarmaps_v1/clem_bw' +
'/' + zoom + '/' + normalizedCoord.x + '/' +
(bound - normalizedCoord.y - 1) + '.jpg';
},
tileSize: new google.maps.Size(TILE_SIZE, TILE_SIZE),
maxZoom: 9,
minZoom: 0,
radius: 1738000,
name: 'MyMap'
});
mapType.projection = new MercatorProjection(); // Remove and Markers Will Show Correctly
var mapOptions = {
zoom: 3,
center: chicago
};
map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);
map.mapTypes.set('mymap', mapType);
map.setMapTypeId('mymap');
var coordInfoWindow = new google.maps.InfoWindow();
coordInfoWindow.setContent(createInfoWindowContent());
coordInfoWindow.setPosition(chicago);
coordInfoWindow.open(map);
google.maps.event.addListener(map, 'zoom_changed', function() {
coordInfoWindow.setContent(createInfoWindowContent());
coordInfoWindow.open(map);
});
for (var lat=-80; lat<85; lat+=10) {
for (var lng=-180; lng<180; lng+=10) {
var markerlatlng = new google.maps.LatLng(lat, lng);
var marker = new google.maps.Marker({
position: markerlatlng,
map: map,
title: "Moon Marker"
});
}
}
}
google.maps.event.addDomListener(window, 'load', initialize);
</script>
</head>
<body>
<div id="map-canvas"></div>
</body>
</html>
There is nothing wrong with the projection, there seems to be a bug with the rendering of the markers, the missing markers are covered by tiles.
Set the optimized-option of the markers to false and all markers will appear.
I'm using the Google Maps API to embed a map in a web page. The map fills the entire screen, but there's ~400px worth of content on the left side of the screen that mostly covers the map. When I want to pan the map, that area to the left should be treated as though it isn't visible.
I came up with the following code to calculate the "usable part" of the map, which I'd like to be 50px in from the map's edge, and should also avoid the 400px area to the left side of the map:
panIfNotClose = function(latLngs){
if(latLngs.length === 0)
return;
// Get some important values that may not be available yet
// http://stackoverflow.com/a/2833015/802335
var bounds = map.getBounds();
if(!bounds)
return setTimeout(panIfNotClose.bind(this, latLngs), 10);
var overlay = new google.maps.OverlayView();
overlay.draw = function(){};
overlay.setMap(map);
var proj = overlay.getProjection();
if(!proj)
return setTimeout(panIfNotClose.bind(this, latLngs), 10);
// Calculate the "usable part" of the map
var center = bounds.getCenter();
var northEast = bounds.getNorthEast();
var southWest = bounds.getSouthWest();
var northEastPx = proj.fromLatLngToContainerPixel(northEast);
var southWestPx = proj.fromLatLngToContainerPixel(southWest);
var menuPadding = 400;
var newNorthEastPx = new google.maps.Point(northEastPx.x + 50, northEastPx.y + 50);
var newSouthWestPx = new google.maps.Point(southWestPx.x - (50 + menuPadding), southWestPx.y - 50);
var newNorthEast = proj.fromContainerPixelToLatLng(newNorthEastPx);
var newSouthWest = proj.fromContainerPixelToLatLng(newSouthWestPx);
var centerBounds = new google.maps.LatLngBounds(newSouthWest, newSouthWest);
// Decide if any of the new LatLngs are far enough away that the map should move
var shouldMove = false;
var targetBounds = new google.maps.LatLngBounds();
for(var i = 0; i < latLngs.length; i++){
targetBounds.extend(latLngs[i]);
if(!centerBounds.contains(latLngs[i]))
shouldMove = true;
}
// If the LatLngs aren't all near the center of the map, pan to it
if(latLngs.length === 1){
if(shouldMove || map.getZoom() !== 18)
map.panTo(latLngs[0]);
map.setZoom(18);
}else{
var targetZoom = Math.min(getBoundsZoomLevel(targetBounds), 18);
if(shouldMove || map.getZoom() !== targetZoom)
map.panTo(targetBounds.getCenter());
map.setZoom(targetZoom);
}
}
This code should test the "valid" area to make sure all the given LatLngs fit inside, but it doesn't yet make any changes to panTo to "move" the center 200px to the right to account for the 400px worth of content on the left.
The code doesn't work as I intended, but I'm not sure why. I suspect I'm probably doing something wrong when converting from a LatLng to a Point or vice-versa. I may also be doing far more work than is necessary, although I can't think of a way to simplify it.
This turned out to be a pretty simple mistake. In the line var centerBounds = new google.maps.LatLngBounds(newSouthWest, newSouthWest);, I accidentally used the same LatLng twice, rather than the northeast and southwest corners. I also had a couple simple arithmetic issues when calculating newNorthEastPx and newSouthWestPx. Drawing a google.maps.Rectangle using centerBounds helped work that out quickly and easily. For anyone interested, here's the end result:
function panIfNotClose(latLngs, zoomOnly){
if(latLngs.length === 0)
return;
if(typeof latLngs !== "object")
latLngs = [latLngs];
// Calculate the "middle half" of the map
var bounds = map.getBounds();
if(!bounds) // http://stackoverflow.com/a/2833015/802335
return setTimeout(panIfNotClose.bind(this, latLngs, zoomOnly), 10);
var overlay = new google.maps.OverlayView();
overlay.draw = function(){};
overlay.setMap(map);
var proj = overlay.getProjection();
if(!proj)
return setTimeout(panIfNotClose.bind(this, latLngs, zoomOnly), 10);
var center = bounds.getCenter();
var northEast = bounds.getNorthEast();
var southWest = bounds.getSouthWest();
var northEastPx = proj.fromLatLngToContainerPixel(northEast);
var southWestPx = proj.fromLatLngToContainerPixel(southWest);
var menuPadding = 0;
if($navBar.css("display") !== "none")
menuPadding = 400;
var newNorthEastPx = new google.maps.Point(northEastPx.x - 100, northEastPx.y + 100);
var newSouthWestPx = new google.maps.Point(southWestPx.x + (100 + menuPadding), southWestPx.y - 100);
var newNorthEast = proj.fromContainerPixelToLatLng(newNorthEastPx);
var newSouthWest = proj.fromContainerPixelToLatLng(newSouthWestPx);
centerBounds = new google.maps.LatLngBounds(newSouthWest, newNorthEast);
var shouldMove = false;
var targetBounds = new google.maps.LatLngBounds();
for(var i = 0; i < latLngs.length; i++){
targetBounds.extend(latLngs[i]);
if(!centerBounds.contains(latLngs[i]))
shouldMove = true;
}
// If the marker isn't near the center of the map, pan to it
if(latLngs.length === 1){
if(!zoomOnly && (shouldMove || map.getZoom() !== 18))
map.panTo(correctCenter(latLngs[0], proj));
map.setZoom(18);
}else{
var targetZoom = Math.min(getBoundsZoomLevel(targetBounds), 18);
if(!zoomOnly && (shouldMove || map.getZoom() !== targetZoom))
map.panTo(correctCenter(targetBounds.getCenter(), proj));
map.setZoom(targetZoom);
}
}
function correctCenter(latLng, proj){
// $navBar references a jQuery pointer to a DOM element
if($navBar.css("display") === "none")
return latLng;
var latLngPx = proj.fromLatLngToContainerPixel(latLng);
var newLatLngPx = new google.maps.Point(latLngPx.x - 200, latLngPx.y)
return proj.fromContainerPixelToLatLng(newLatLngPx);
}
// Adapted from http://stackoverflow.com/a/13274361/802335
function getBoundsZoomLevel(bounds){
// $mapCanvas is a jQuery reference to the div containing the Google Map
var mapDim = {
width: $mapCanvas.width() * .6,
height: $mapCanvas.height() * .6
};
var ZOOM_MAX = 18;
function latRad(lat){
var sin = Math.sin(lat * Math.PI / 180);
var radX2 = Math.log((1 + sin) / (1 - sin)) / 2;
return Math.max(Math.min(radX2, Math.PI), -Math.PI) / 2;
}
function zoom(mapPx, fraction){
return Math.floor(Math.log(mapPx / 256 / fraction) / Math.LN2);
}
var ne = bounds.getNorthEast();
var sw = bounds.getSouthWest();
var latFraction = (latRad(ne.lat()) - latRad(sw.lat())) / Math.PI;
var lngDiff = ne.lng() - sw.lng();
var lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360;
var latZoom = zoom(mapDim.height, latFraction);
var lngZoom = zoom(mapDim.width, lngFraction);
return Math.min(latZoom, lngZoom, ZOOM_MAX);
}
I've been looking for a good solution to animate a marker using the Google Maps JavaScript API V3. The marker will be animated on a predetermined path defined using latitude and longitude coordinates.
For all the research I've done, I still can't find a solution that is compatible with version 3 of the JavaScript Google Maps API. Looking at this earlier StackOverflow post, apparently it was possible to animate using Version 2 of the API, using GRoute and setting the position of a marker to points along the route using a timer.
Here is the code that was previously suggested. I understand how it works logically, but I have no idea how this could be ported to work with version 3 of the Google Maps API:
function moveToStep(yourmarker,yourroute,c) {
if {yourroute.getNumSteps() > c) {
yourmarker.setLatLng(yourroute.getStep(c).getLatLng());
window.setTimeout(function(){
moveToStep(yourmarker,yourroute,c+1);
},500);
}
}
moveToStep(marker,route,0);
There is no mention of GRoute, getNumSteps (which I'm supposing returns the number of coordinates defined on a given route, setLatLng (I believe which gets the latitude and longitude coordinates of the marker), or moveToStep (which actually moves the marker) in the version 3 full documentation and reference.
It seems that Google has completely rewritten the API from version 2 to version 3, as these functions (which seem to be pretty basic) all have either been removed or renamed (I'm not sure which.)
The only mention of animations I have ever seen in version 3 of the JavaScript API has been for animating the markers when they first appear on the map, by making them either BOUNCE or DROP. However, these do not actually move the latitude/longitude coordinates of the marker, merely the manner in which they are placed on the map. These two marker animations are mentioned in the API reference here.
On the same aforementioned StackOverflow post, a link was provided to a working example of marker animation using the JavaScript API. However, as a commenter pointed out, the animation was done using an earlier version of the library (available here.)
Ultimately, I guess I have two questions:
1: Is it possible to animate markers along a given path using the Google Maps V3 API?
and if not, then
2: Will I be forced to use a deprecated library to achieve this, or are there any other known solutions?
Thank you very much for any contribution that may help solve this problem!
Yes. it is possible to animate a marker along a route from the DirectionsService in v3.
This example uses a version of Mike Williams' epoly library ported to v3.
proof of concept fiddle
code snippet:
var map;
var directionDisplay;
var directionsService;
var stepDisplay;
var markerArray = [];
var position;
var marker = null;
var polyline = null;
var poly2 = null;
var speed = 0.000005,
wait = 1;
var infowindow = null;
var myPano;
var panoClient;
var nextPanoId;
var timerHandle = null;
function createMarker(latlng, label, html) {
// alert("createMarker("+latlng+","+label+","+html+","+color+")");
var contentString = '<b>' + label + '</b><br>' + html;
var marker = new google.maps.Marker({
position: latlng,
map: map,
title: label,
zIndex: Math.round(latlng.lat() * -100000) << 5
});
marker.myname = label;
// gmarkers.push(marker);
google.maps.event.addListener(marker, 'click', function() {
infowindow.setContent(contentString);
infowindow.open(map, marker);
});
return marker;
}
function initialize() {
infowindow = new google.maps.InfoWindow({
size: new google.maps.Size(150, 50)
});
// Instantiate a directions service.
directionsService = new google.maps.DirectionsService();
// Create a map and center it on Manhattan.
var myOptions = {
zoom: 13,
mapTypeId: google.maps.MapTypeId.ROADMAP
}
map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
address = 'new york'
geocoder = new google.maps.Geocoder();
geocoder.geocode({
'address': address
}, function(results, status) {
map.setCenter(results[0].geometry.location);
});
// Create a renderer for directions and bind it to the map.
var rendererOptions = {
map: map
}
directionsDisplay = new google.maps.DirectionsRenderer(rendererOptions);
// Instantiate an info window to hold step text.
stepDisplay = new google.maps.InfoWindow();
polyline = new google.maps.Polyline({
path: [],
strokeColor: '#FF0000',
strokeWeight: 3
});
poly2 = new google.maps.Polyline({
path: [],
strokeColor: '#FF0000',
strokeWeight: 3
});
}
var steps = []
function calcRoute() {
if (timerHandle) {
clearTimeout(timerHandle);
}
if (marker) {
marker.setMap(null);
}
polyline.setMap(null);
poly2.setMap(null);
directionsDisplay.setMap(null);
polyline = new google.maps.Polyline({
path: [],
strokeColor: '#FF0000',
strokeWeight: 3
});
poly2 = new google.maps.Polyline({
path: [],
strokeColor: '#FF0000',
strokeWeight: 3
});
// Create a renderer for directions and bind it to the map.
var rendererOptions = {
map: map
}
directionsDisplay = new google.maps.DirectionsRenderer(rendererOptions);
var start = document.getElementById("start").value;
var end = document.getElementById("end").value;
var travelMode = google.maps.DirectionsTravelMode.DRIVING
var request = {
origin: start,
destination: end,
travelMode: travelMode
};
// Route the directions and pass the response to a
// function to create markers for each step.
directionsService.route(request, function(response, status) {
if (status == google.maps.DirectionsStatus.OK) {
directionsDisplay.setDirections(response);
var bounds = new google.maps.LatLngBounds();
var route = response.routes[0];
startLocation = new Object();
endLocation = new Object();
// For each route, display summary information.
var path = response.routes[0].overview_path;
var legs = response.routes[0].legs;
for (i = 0; i < legs.length; i++) {
if (i == 0) {
startLocation.latlng = legs[i].start_location;
startLocation.address = legs[i].start_address;
// marker = google.maps.Marker({map:map,position: startLocation.latlng});
marker = createMarker(legs[i].start_location, "start", legs[i].start_address, "green");
}
endLocation.latlng = legs[i].end_location;
endLocation.address = legs[i].end_address;
var steps = legs[i].steps;
for (j = 0; j < steps.length; j++) {
var nextSegment = steps[j].path;
for (k = 0; k < nextSegment.length; k++) {
polyline.getPath().push(nextSegment[k]);
bounds.extend(nextSegment[k]);
}
}
}
polyline.setMap(map);
map.fitBounds(bounds);
// createMarker(endLocation.latlng,"end",endLocation.address,"red");
map.setZoom(18);
startAnimation();
}
});
}
var step = 50; // 5; // metres
var tick = 100; // milliseconds
var eol;
var k = 0;
var stepnum = 0;
var speed = "";
var lastVertex = 1;
//=============== animation functions ======================
function updatePoly(d) {
// Spawn a new polyline every 20 vertices, because updating a 100-vertex poly is too slow
if (poly2.getPath().getLength() > 20) {
poly2 = new google.maps.Polyline([polyline.getPath().getAt(lastVertex - 1)]);
// map.addOverlay(poly2)
}
if (polyline.GetIndexAtDistance(d) < lastVertex + 2) {
if (poly2.getPath().getLength() > 1) {
poly2.getPath().removeAt(poly2.getPath().getLength() - 1)
}
poly2.getPath().insertAt(poly2.getPath().getLength(), polyline.GetPointAtDistance(d));
} else {
poly2.getPath().insertAt(poly2.getPath().getLength(), endLocation.latlng);
}
}
function animate(d) {
// alert("animate("+d+")");
if (d > eol) {
map.panTo(endLocation.latlng);
marker.setPosition(endLocation.latlng);
return;
}
var p = polyline.GetPointAtDistance(d);
map.panTo(p);
marker.setPosition(p);
updatePoly(d);
timerHandle = setTimeout("animate(" + (d + step) + ")", tick);
}
function startAnimation() {
eol = google.maps.geometry.spherical.computeLength(polyline.getPath());
map.setCenter(polyline.getPath().getAt(0));
// map.addOverlay(new google.maps.Marker(polyline.getAt(0),G_START_ICON));
// map.addOverlay(new GMarker(polyline.getVertex(polyline.getVertexCount()-1),G_END_ICON));
// marker = new google.maps.Marker({location:polyline.getPath().getAt(0)} /* ,{icon:car} */);
// map.addOverlay(marker);
poly2 = new google.maps.Polyline({
path: [polyline.getPath().getAt(0)],
strokeColor: "#0000FF",
strokeWeight: 10
});
// map.addOverlay(poly2);
setTimeout("animate(50)", 2000); // Allow time for the initial map display
}
//=============== ~animation funcitons =====================
google.maps.event.addDomListener(window, "load", initialize);
/*********************************************************************\
* *
* epolys.js by Mike Williams *
* updated to API v3 by Larry Ross *
* *
* A Google Maps API Extension *
* *
* Adds various Methods to google.maps.Polygon and google.maps.Polyline *
* *
* .Contains(latlng) returns true is the poly contains the specified *
* GLatLng *
* *
* .Area() returns the approximate area of a poly that is *
* not self-intersecting *
* *
* .Distance() returns the length of the poly path *
* *
* .Bounds() returns a GLatLngBounds that bounds the poly *
* *
* .GetPointAtDistance() returns a GLatLng at the specified distance *
* along the path. *
* The distance is specified in metres *
* Reurns null if the path is shorter than that *
* *
* .GetPointsAtDistance() returns an array of GLatLngs at the *
* specified interval along the path. *
* The distance is specified in metres *
* *
* .GetIndexAtDistance() returns the vertex number at the specified *
* distance along the path. *
* The distance is specified in metres *
* Returns null if the path is shorter than that *
* *
* .Bearing(v1?,v2?) returns the bearing between two vertices *
* if v1 is null, returns bearing from first to last *
* if v2 is null, returns bearing from v1 to next *
* *
* *
***********************************************************************
* *
* This Javascript is provided by Mike Williams *
* Blackpool Community Church Javascript Team *
* http://www.blackpoolchurch.org/ *
* http://econym.org.uk/gmap/ *
* *
* This work is licenced under a Creative Commons Licence *
* http://creativecommons.org/licenses/by/2.0/uk/ *
* *
***********************************************************************
* *
* Version 1.1 6-Jun-2007 *
* Version 1.2 1-Jul-2007 - fix: Bounds was omitting vertex zero *
* add: Bearing *
* Version 1.3 28-Nov-2008 add: GetPointsAtDistance() *
* Version 1.4 12-Jan-2009 fix: GetPointsAtDistance() *
* Version 3.0 11-Aug-2010 update to v3 *
* *
\*********************************************************************/
google.maps.LatLng.prototype.latRadians = function() {
return this.lat() * Math.PI / 180;
}
google.maps.LatLng.prototype.lngRadians = function() {
return this.lng() * Math.PI / 180;
}
// === A method which returns a GLatLng of a point a given distance along the path ===
// === Returns null if the path is shorter than the specified distance ===
google.maps.Polyline.prototype.GetPointAtDistance = function(metres) {
// some awkward special cases
if (metres == 0) return this.getPath().getAt(0);
if (metres < 0) return null;
if (this.getPath().getLength() < 2) return null;
var dist = 0;
var olddist = 0;
for (var i = 1;
(i < this.getPath().getLength() && dist < metres); i++) {
olddist = dist;
dist += google.maps.geometry.spherical.computeDistanceBetween(this.getPath().getAt(i), this.getPath().getAt(i - 1));
}
if (dist < metres) {
return null;
}
var p1 = this.getPath().getAt(i - 2);
var p2 = this.getPath().getAt(i - 1);
var m = (metres - olddist) / (dist - olddist);
return new google.maps.LatLng(p1.lat() + (p2.lat() - p1.lat()) * m, p1.lng() + (p2.lng() - p1.lng()) * m);
}
// === A method which returns the Vertex number at a given distance along the path ===
// === Returns null if the path is shorter than the specified distance ===
google.maps.Polyline.prototype.GetIndexAtDistance = function(metres) {
// some awkward special cases
if (metres == 0) return this.getPath().getAt(0);
if (metres < 0) return null;
var dist = 0;
var olddist = 0;
for (var i = 1;
(i < this.getPath().getLength() && dist < metres); i++) {
olddist = dist;
dist += google.maps.geometry.spherical.computeDistanceBetween(this.getPath().getAt(i), this.getPath().getAt(i - 1));
}
if (dist < metres) {
return null;
}
return i;
}
html {
height: 100%;
}
body {
height: 100%;
margin: 0px;
font-family: Helvetica, Arial;
}
<script src="https://maps.googleapis.com/maps/api/js?libraries=geometry&key=AIzaSyCkUOdZ5y7hMm0yrcCQoCvLwzdM6M8s5qk"></script>
<div id="tools">
start:
<input type="text" name="start" id="start" value="union square, NY" />end:
<input type="text" name="end" id="end" value="times square, NY" />
<input type="submit" onclick="calcRoute();" />
</div>
<div id="map_canvas" style="width:100%;height:100%;"></div>
One approach is to generate a series of locations along the route (for example 500 distinct locations) and then call marker.setPosition(nextLocation) every X milliseconds using, for example, setTimeout.
Alternatively, you can use a symbol on your polyline, setting the percentage along the path, again in a setTimeout approach. This has the advantage you don't need to compute the locations, you can just slowly move it some tiny percentage along each X milliseconds.