Google Maps API V3: limit map bounds [duplicate] - google-maps

This question already has answers here:
How do I limit panning in Google maps API V3?
(15 answers)
Closed 8 years ago.
I'm trying to set bounds where you can drag the map using Google Maps API V3
Here is the solution for V2 http://econym.org.uk/gmap/example_range.htm that works pretty well.
However with API V3 it isn't so good: when you use the same checkbounds() function, the map is twitching when you reach the bound while map.setCenter() changes the center of the map.
How to fix it? What is the solution for API V3?

I've had the same problem, but this should sort it out (it's the same function, the event to listen to is changed from 'move' or 'drag' to 'center_changed', works like a charm!:
google.maps.event.addListener(map,'center_changed',function() { checkBounds(); });
function checkBounds() {
if(! allowedBounds.contains(map.getCenter())) {
var C = map.getCenter();
var X = C.lng();
var Y = C.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));
}
}

You might also need to consider wrapping coordinates, curve distortion, and centerizing to the bound dimensions if the map resizes or zooms in/out. This is especially required if your bounds takes up a large percentage of the entire map (eg. like a continent).
One of the problems with checkBounds() though, is that it doesn't take into account that latitude values close to the north/south poles, which have non-linear distortion which make limiting the bounds in-accurate (I use approximate magic number multipliers that won't work in all situations). By right, you should first convert the bounds to linear 2d world coordinates to see how far off the bounds it is in terms of world coordinates, than map the actual target center point in world coordinate to the target actual latitude position. For longitude values, this doesn't seem like much of issue and the linear clipping approach seems accurate enough, the main issue is with the wrapping of longitude coordinates which is accounted for (somewhat) in the below code.
// Persitant variables
var allowedBounds; // assign something here
var lastValidCenter; // initialize this using map.getCenter()
function checkBounds() { // when bounds changes due to resizing or zooming in/out
var currentBounds = map.getBounds();
if (currentBounds == null) return;
var allowed_ne_lng = allowedBounds.getNorthEast().lng();
var allowed_ne_lat = allowedBounds.getNorthEast().lat();
var allowed_sw_lng = allowedBounds.getSouthWest().lng();
var allowed_sw_lat = allowedBounds.getSouthWest().lat();
var wrap;
var cc = map.getCenter();
var centerH = false;
var centerV = false;
// Check horizontal wraps and offsets
if ( currentBounds.toSpan().lng() > allowedBounds.toSpan().lng() ) {
centerH = true;
}
else { // test positive and negative wrap respectively
wrap = currentBounds.getNorthEast().lng() < cc.lng();
var current_ne_lng = !wrap ? currentBounds.getNorthEast().lng() : allowed_ne_lng +(currentBounds.getNorthEast().lng() + 180 ) + (180 - allowed_ne_lng);
wrap = currentBounds.getSouthWest().lng() > cc.lng();
var current_sw_lng = !wrap ? currentBounds.getSouthWest().lng() : allowed_sw_lng - (180-currentBounds.getSouthWest().lng()) - (allowed_sw_lng+180);
}
// Check vertical wraps and offsets
if ( currentBounds.toSpan().lat() > allowedBounds.toSpan().lat() ) {
centerV = true;
}
else { // test positive and negative wrap respectively
wrap = currentBounds.getNorthEast().lat() < cc.lat(); if (wrap) { alert("WRAp detected top") } // else alert("no wrap:"+currentBounds); wrap = false;
var current_ne_lat = !wrap ? currentBounds.getNorthEast().lat() : allowed_ne_lat + (currentBounds.getNorthEast().lat() +90) + (90 - allowed_ne_lat);
wrap = currentBounds.getSouthWest().lat() > cc.lat(); if (wrap) { alert("WRAp detected btm") } //alert("no wrap:"+currentBounds);
var current_sw_lat = !wrap ? currentBounds.getSouthWest().lat() : allowed_sw_lat - (90-currentBounds.getSouthWest().lat()) - (allowed_sw_lat+90);
}
// Finalise positions
var centerX = cc.lng();
var centerY = cc.lat();
if (!centerH) {
if (current_ne_lng > allowed_ne_lng) centerX -= current_ne_lng-allowed_ne_lng;
if (current_sw_lng < allowed_sw_lng) centerX += allowed_sw_lng-current_sw_lng;
}
else {
centerX = allowedBounds.getCenter().lng();
}
if (!centerV) {
if (current_ne_lat > allowed_ne_lat) {
centerY -= (current_ne_lat-allowed_ne_lat) * 3; // approximation magic numbeer. Adjust as u see fit, or use a more accruate pixel measurement.
}
if (current_sw_lat < allowed_sw_lat) {
centerY += (allowed_sw_lat-current_sw_lat)*2.8; // approximation magic number
}
}
else {
centerY = allowedBounds.getCenter().lat();
}
map.setCenter(lastValidCenter = new google.maps.LatLng(centerY,centerX));
}
function limitBound(bound) // Occurs during dragging, pass allowedBounds to this function in most cases. Requires persistant 'lastValidCenter=map.getCenter()' var reference.
{
var mapBounds = map.getBounds();
if ( mapBounds.getNorthEast().lng() >= mapBounds.getSouthWest().lng() && mapBounds.getNorthEast().lat() >= mapBounds.getSouthWest().lat() // ensure no left/right, top/bottom wrapping
&& bound.getNorthEast().lat() > mapBounds.getNorthEast().lat() // top
&& bound.getNorthEast().lng() > mapBounds.getNorthEast().lng() // right
&& bound.getSouthWest().lat() < mapBounds.getSouthWest().lat() // bottom
&& bound.getSouthWest().lng() < mapBounds.getSouthWest().lng()) // left
{
lastValidCenter=map.getCenter(); // valid case, set up new valid center location
}
// if (bound.contains(map.getCenter()))
// {
map.panTo(lastValidCenter);
// }
}
// Google map listeners
google.maps.event.addListener(map, 'zoom_changed', function() {
//var zoom = map.getZoom();
checkBounds();
});
google.maps.event.addListener(map, "bounds_changed", function() {
checkBounds();
});
google.maps.event.addListener(map, 'center_changed', function() {
limitBound(allowedBounds);
});
p.s For checkBounds(), to get proper 2d world coordinate from the map's center, given 2 lat/lng values, use map.getProjection().fromLatLngToPoint(). Compare the 2 points, find the linear difference between them, and map the difference in world coordinates back to lat/lng using map.getProjection().fromPointToLatLng(). This will give you accurate clip offsets in lat/lng units.

This script gets the initial bounds (allowedBounds) and limit the bounds on drag and zoom_changed. Also the zoom is limited on < 7.
var allowedBounds = false;
google.maps.event.addListener(map, 'idle', function() {
if (!allowedBounds) {
allowedBounds = map.getBounds();
}
});
google.maps.event.addListener(map, 'drag', checkBounds);
google.maps.event.addListener(map, 'zoom_changed', checkBounds);
function checkBounds() {
if (map.getZoom() < 7) map.setZoom(7);
if (allowedBounds) {
var allowed_ne_lng = allowedBounds.getNorthEast().lng();
var allowed_ne_lat = allowedBounds.getNorthEast().lat();
var allowed_sw_lng = allowedBounds.getSouthWest().lng();
var allowed_sw_lat = allowedBounds.getSouthWest().lat();
var currentBounds = map.getBounds();
var current_ne_lng = currentBounds.getNorthEast().lng();
var current_ne_lat = currentBounds.getNorthEast().lat();
var current_sw_lng = currentBounds.getSouthWest().lng();
var current_sw_lat = currentBounds.getSouthWest().lat();
var currentCenter = map.getCenter();
var centerX = currentCenter.lng();
var centerY = currentCenter.lat();
if (current_ne_lng > allowed_ne_lng) centerX = centerX-(current_ne_lng-allowed_ne_lng);
if (current_ne_lat > allowed_ne_lat) centerY = centerY-(current_ne_lat-allowed_ne_lat);
if (current_sw_lng < allowed_sw_lng) centerX = centerX+(allowed_sw_lng-current_sw_lng);
if (current_sw_lat < allowed_sw_lat) centerY = centerY+(allowed_sw_lat-current_sw_lat);
map.setCenter(new google.maps.LatLng(centerY,centerX));
}
}

Thanks #sairafi. Your answer got me very close. I was getting an error where getBounds was undefined, so I wrapped it in another listener to make sure that the map was fully loaded first.
google.maps.event.addListenerOnce(map, 'tilesloaded', function() {
allowedBounds = map.getBounds();
google.maps.event.addListener(map,'center_changed',function() { checkBounds(allowedBounds); });
});
// Limit map area
function checkBounds(allowedBounds) {
if(!allowedBounds.contains(map.getCenter())) {
var C = map.getCenter();
var X = C.lng();
var Y = C.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));
}
}

southWest = new google.maps.LatLng(48.59475380744011,22.247364044189453);
northEast = new google.maps.LatLng(48.655344320891444,22.352420806884766);
var limBound = new google.maps.LatLngBounds(southWest,northEast);
var lastCenter;
var option = {zoom:15,
center: limBound.getCenter(),
mapTypeId: google.maps.MapTypeId.ROADMAP};
var map = new google.maps.Map(document.getElementById('divMap'),option);
google.maps.event.addListener(map,'zoom_changed', function() {
minZoom(15);
});
google.maps.event.addListener(map,'drag',function(e){
limitBound(limBound);
});
function minZoom(minZoom){
if (map.getZoom()<minZoom)
{map.setZoom(minZoom);}
};
function limitBound(bound)
{
if (bound.getNorthEast().lat() > map.getBounds().getNorthEast().lat()
&& bound.getNorthEast().lng() > map.getBounds().getNorthEast().lng()
&& bound.getSouthWest().lat() < map.getBounds().getSouthWest().lat()
&& bound.getSouthWest().lng() < map.getBounds().getSouthWest().lng())
{
lastCenter=map.getCenter();
$('#divText').text(lastCenter.toString());
}
if (bound.contains(map.getCenter()))
{
map.setCenter(lastCenter);
}
}

pls check this Google Maps API v3: Can I setZoom after fitBounds?
map.fitBounds(mapBounds);

#sairafi and #devin
thanks for both your answers. I could not get this to work in Chrome/Windows 7 because the .comtains() check evlauated to both true and false as soon as you hit the boundary.
So I changed the setCenter() at the bottom to panTo()
The second problem was that if you establish boundaries on initial load, you have to use the google.maps.event.addListenerOnce(map,'idle'...) event otherwise it keeps resetting the boundaries to the currently viewable map.
Finally I do use the drag event for tracking centers, for some reason that worked more smoothly.
The resulting code is this:
google.maps.event.addListenerOnce(map,'idle',function() {
allowedBounds = map.getBounds();
});
google.maps.event.addListener(map,'drag',function() {
checkBounds();
});
function checkBounds() {
if(! allowedBounds.contains(map.getCenter()))
{
var C = map.getCenter();
var X = C.lng();
var Y = C.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.panTo(new google.maps.LatLng(Y,X));
}
}

Related

Html5 canvas scrolling vertically and horizontally

<!DOCTYPE html>
<html>
<head>
<style type="text/css">
#canvasOne
{
border: 1px solid black;
}
</style>
<script src="http://code.jquery.com/jquery-1.10.2.js" type="text/javascript"></script>
</head>
<body>
<div align="center">
<canvas id="canvasOne">
</canvas>
</div>
<script type="text/javascript">
var myCanvas = document.getElementById("canvasOne");
var myContext = myCanvas.getContext("2d");
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
init();
var numShapes;
var shapes;
var dragIndex;
var dragging;
var mouseX;
var mouseY;
var dragHoldX;
var dragHoldY;
var timer;
var targetX;
var targetY;
var easeAmount;
var bgColor;
var nodes;
var colorArr;
function init()
{
myCanvas.width = $(window).width() - 200;
myCanvas.height = $(window).height() - 200;
shapes = [];
nodes = ["0;Person;24828760;Alok Kumar;Gorakhpur;#F44336;28",
"0;Suspect;04/Dec/2016;4;Suman_Biswas;#3F51B5;20","1;Rule;4;Apparent Means;3 Parameter;#EEFF41;20",
"0;Policy;36QA649749;In-Force;Quarterly;#FF9800;20","3;Product;Pension;Saral Pension;SRPEN;#795548;20","3;Payment;Cheque;Realized;Lucknow;#0091EA;20",
"0;Policy;162348873;Lapsed;Quarterly;#FF9800;20","6;Product;Pension;Life-Long Pension;LLPP;#795548;20","6;Payment;Cheque;Realized;Gorakhpur;#0091EA;20",
"0;Policy;1EQF178639;Lapsed;Monthly;#FF9800;20","9;Product;Life;Shield;SHIELDA;#795548;20","9;Payment;Demand Draft;Realized;Lucknow;#0091EA;20"];
numShapes = nodes.length;
makeShapes();
drawScreen();
myCanvas.addEventListener("mousedown", mouseDownListener, false);
}
//drawing
function makeShapes()
{
var tempX;
var tempY;
for(var i = 0; i < numShapes; i++)
{
var centerX = myCanvas.width/2;
var centerY = myCanvas.height/2;
var nodeColor = nodes[i].split(";")[5];
var nodeRadius = nodes[i].split(";")[6];
var nodeConnect = nodes[i].split(";")[0];
if(i == 0)//center of circle
{
tempX = centerX
tempY = centerY;
}
else
{
//tempX = Math.random() * (myCanvas.width - tempRadius);
//tempY = Math.random() * (myCanvas.height - tempRadius);
//var x = x0 + r * Math.cos(2 * Math.PI * i / items);
//var y = y0 + r * Math.sin(2 * Math.PI * i / items);
//250 is the distance from center node to outside nodes it can be actual radius in degrees
tempX = shapes[nodeConnect].x + 300 * Math.cos(2 * Math.PI * i / numShapes);
tempY = shapes[nodeConnect].y + 300 * Math.sin(2 * Math.PI * i / numShapes);
}
tempShape = {x: tempX, y: tempY, rad: nodeRadius, color: nodeColor, text: nodes[i]};
shapes.push(tempShape);
}
}
//drawing both shape (line and circle) and screen
function drawScreen()
{
myContext.fillStyle = "#ffffff";
myContext.fillRect(0, 0, myCanvas.width, myCanvas.height);
drawShapes();
}
function drawShapes()
{
//line
for(var i = 1; i < numShapes; i++)
{
myContext.beginPath();
myContext.strokeStyle = "#B2B19D";
var nodeConnect = nodes[i].split(";")[0];
myContext.moveTo(shapes[nodeConnect].x, shapes[nodeConnect].y);
myContext.lineTo(shapes[i].x, shapes[i].y);
myContext.stroke();
}
//circle
for(var i = 0; i < numShapes; i++)
{
myContext.fillStyle = shapes[i].color;
myContext.beginPath();
myContext.arc(shapes[i].x, shapes[i].y, shapes[i].rad, 0, 2*Math.PI, false);
myContext.closePath();
myContext.fill();
}
//text
for(var i = 0; i < numShapes; i++)
{
myContext.beginPath();
myContext.font = '10pt Arial';
myContext.fillStyle = 'black';
var textarr = shapes[i].text.split(";");
myContext.fillText(textarr[1], shapes[i].x + 30, shapes[i].y - 24);
/*myContext.fillText(textarr[2], shapes[i].x + 30, shapes[i].y + 1);
myContext.fillText(textarr[3], shapes[i].x + 30, shapes[i].y + 22);
myContext.fillText(textarr[4], shapes[i].x + 30, shapes[i].y + 44);*/
myContext.closePath();
myContext.fill();
}
}
//animation
function mouseDownListener(evt)
{
var highestIndex = -1;
var bRect = myCanvas.getBoundingClientRect();
mouseX = (evt.clientX - bRect.left) * (myCanvas.width/bRect.width);
mouseY = (evt.clientY - bRect.top) * (myCanvas.height/bRect.height);
for(var i = 0; i < numShapes; i++)
{
if(hitTest(shapes[i], mouseX, mouseY))
{
dragging = true;
if(i > highestIndex)
{
dragHoldX = mouseX - shapes[i].x;
dragHoldY = mouseY - shapes[i].y;
highestIndex = i;
dragIndex = i;
}
}
}
if(dragging)
{
window.addEventListener("mousemove", mouseMoveListener, false);
}
myCanvas.removeEventListener("mousedown", mouseDownListener, false);
window.addEventListener("mouseup", mouseUpListener, false);
if(evt.preventDefault)
{
evt.preventDefault;
}
return false;
}
function mouseMoveListener(evt)
{
var shapeRad = shapes[dragIndex].rad;
var minX = shapeRad;
var maxX = myCanvas.width - shapeRad;
var minY = shapeRad;
var maxY = myCanvas.height - shapeRad;
//get mouse position correctly
var bRect = myCanvas.getBoundingClientRect();
mouseX = (evt.clientX - bRect.left)*(myCanvas.width / bRect.width);
mouseY = (evt.clientY - bRect.top)*(myCanvas.height / bRect.height);
//clamp x and y position to prevent object from dragging outside canvas
posX = mouseX - dragHoldX;
posX = (posX < minX) ? minX : ((posX > maxX) ? maxX : posX);
posY = mouseY - dragHoldY;
posY = (posY < minY) ? minY : ((posY > maxY) ? maxY : posY);
shapes[dragIndex].x = posX;
shapes[dragIndex].y = posY;
drawScreen();
}
function mouseUpListener(evt)
{
myCanvas.addEventListener("mousedown", mouseDownListener, false);
window.removeEventListener("mouseup", mouseUpListener, false);
if(dragging)
{
dragging = false;
window.removeEventListener("mousemove", mouseMoveListener, false);
}
}
function hitTest(shape, mx, my)
{
var dx = mx - shape.x;
var dy = my - shape.y;
return(dx * dx + dy * dy < shape.rad * shape.rad);
}
</script>
</body>
</html>
The following canvas animation creates nodes and edges. However due
to space constraint, some of the nodes are not visible due to canvas
height and width. Even adding overflow css to canvas dosen't help as
i am not able to scroll.
<canvas> context doesn't have a built-in scroll method.
You then have multiple ways to circumvent this limitation.
The first one, is as in #markE's answer, to scale your context's matrix so that your drawings fit into the required space. You could also refactor your code so that all coordinates are relative to the canvas size.
This way, you won't need scrollbars and all your drawings will just be scaled appropriately, which is the desirable behavior in most common cases.
But if you really need to have some scrolling feature, here are some ways :
The easiest and most recommended one : let the browser handle it.
You will have to set the size of your canvas to the maximum of your drawings, and wrap it in an other element which will scroll. By setting the overflow:auto css property on the container, our scrollbars appear and we have our scrolling feature.
In following example, the canvas is 5000px wide and the container 200px.
var ctx = canvas.getContext('2d');
ctx.textAlign = 'center';
for (var w = 0; w < canvas.width; w += 100) {
for (var h = 0; h < canvas.height; h += 100) {
ctx.fillText(w + ',' + h, w, h);
}
}
#container {
width: 200px;
height: 200px;
overflow: auto;
border: 1px solid;
}
canvas{
display: block;
}
<div id="container">
<canvas id="canvas" height="5000" width="5000"></canvas>
</div>
Main advantages :
easily implemented.
users are used to these scrollbars.
Main caveats :
You're limited by canvas maximum sizes.
If your canvas is animated, you'll also draw for each frame parts of the canvas that aren't visible.
You have small control on scrollbars look and you'll still have to implement drag-to-scroll feature yourself for desktop browsers.
A second solution, is to implement this feature yourself, using canvas transform methods : particularly translate, transform and setTransform.
Here is an example :
var ctx = canvas.getContext('2d');
var app = {};
// the total area of our drawings, can be very large now
app.WIDTH = 5000;
app.HEIGHT = 5000;
app.draw = function() {
// reset everything (clears the canvas + transform + fillStyle + any other property of the context)
canvas.width = canvas.width;
// move our context by the inverse of our scrollbars' left and top property
ctx.setTransform(1, 0, 0, 1, -app.scrollbars.left, -app.scrollbars.top);
ctx.textAlign = 'center';
// draw only the visible area
var visibleLeft = app.scrollbars.left;
var visibleWidth = visibleLeft + canvas.width;
var visibleTop = app.scrollbars.top
var visibleHeight = visibleTop + canvas.height;
// you probably will have to make other calculations than these ones to get your drawings
// to draw only where required
for (var w = visibleLeft; w < visibleWidth + 50; w += 100) {
for (var h = visibleTop; h < visibleHeight + 50; h += 100) {
var x = Math.round((w) / 100) * 100;
var y = Math.round((h) / 100) * 100;
ctx.fillText(x + ',' + y, x, y);
}
}
// draw our scrollbars on top if needed
app.scrollbars.draw();
}
app.scrollbars = function() {
var scrollbars = {};
// initial position
scrollbars.left = 0;
scrollbars.top = 0;
// a single constructor for both horizontal and vertical
var ScrollBar = function(vertical) {
var that = {
vertical: vertical
};
that.left = vertical ? canvas.width - 10 : 0;
that.top = vertical ? 0 : canvas.height - 10;
that.height = vertical ? canvas.height - 10 : 5;
that.width = vertical ? 5 : canvas.width - 10;
that.fill = '#dedede';
that.cursor = {
radius: 5,
fill: '#bababa'
};
that.cursor.top = vertical ? that.cursor.radius : that.top + that.cursor.radius / 2;
that.cursor.left = vertical ? that.left + that.cursor.radius / 2 : that.cursor.radius;
that.draw = function() {
if (!that.visible) {
return;
}
// remember to reset the matrix
ctx.setTransform(1, 0, 0, 1, 0, 0);
// you can give it any shape you like, all canvas drawings operations are possible
ctx.fillStyle = that.fill;
ctx.fillRect(that.left, that.top, that.width, that.height);
ctx.beginPath();
ctx.arc(that.cursor.left, that.cursor.top, that.cursor.radius, 0, Math.PI * 2);
ctx.fillStyle = that.cursor.fill;
ctx.fill();
};
// check if we're hovered
that.isHover = function(x, y) {
if (x >= that.left - that.cursor.radius && x <= that.left + that.width + that.cursor.radius &&
y >= that.top - that.cursor.radius && y <= that.top + that.height + that.cursor.radius) {
// we are so record the position of the mouse and set ourself as the one hovered
scrollbars.mousePos = vertical ? y : x;
scrollbars.hovered = that;
that.visible = true;
return true;
}
// we were visible last call and no wheel event is happening
else if (that.visible && !scrollbars.willHide) {
that.visible = false;
// the app should be redrawn
return true;
}
}
return that;
};
scrollbars.horizontal = ScrollBar(0);
scrollbars.vertical = ScrollBar(1);
scrollbars.hovered = null;
scrollbars.dragged = null;
scrollbars.mousePos = null;
// check both of our scrollbars
scrollbars.isHover = function(x, y) {
return this.horizontal.isHover(x, y) || this.vertical.isHover(x, y);
};
// draw both of our scrollbars
scrollbars.draw = function() {
this.horizontal.draw();
this.vertical.draw();
};
// check if one of our scrollbars is visible
scrollbars.visible = function() {
return this.horizontal.visible || this.vertical.visible;
};
// hide it...
scrollbars.hide = function() {
// only if we're not using the mousewheel or dragging the cursor
if (this.willHide || this.dragged) {
return;
}
this.horizontal.visible = false;
this.vertical.visible = false;
};
// get the area's coord relative to our scrollbar
var toAreaCoord = function(pos, scrollBar) {
var sbBase = scrollBar.vertical ? scrollBar.top : scrollBar.left;
var sbMax = scrollBar.vertical ? scrollBar.height : scrollBar.width;
var areaMax = scrollBar.vertical ? app.HEIGHT - canvas.height : app.WIDTH - canvas.width;
var ratio = (pos - sbBase) / (sbMax - sbBase);
return areaMax * ratio;
};
// get the scrollbar's coord relative to our total area
var toScrollCoords = function(pos, scrollBar) {
var sbBase = scrollBar.vertical ? scrollBar.top : scrollBar.left;
var sbMax = scrollBar.vertical ? scrollBar.height : scrollBar.width;
var areaMax = scrollBar.vertical ? app.HEIGHT - canvas.height : app.WIDTH - canvas.width;
var ratio = pos / areaMax;
return ((sbMax - sbBase) * ratio) + sbBase;
}
scrollbars.scroll = function() {
// check which one of the scrollbars is active
var vertical = this.hovered.vertical;
// until where our cursor can go
var maxCursorPos = this.hovered[vertical ? 'height' : 'width'];
var pos = vertical ? 'top' : 'left';
// check that we're not out of the bounds
this.hovered.cursor[pos] = this.mousePos < 0 ? 0 :
this.mousePos > maxCursorPos ? maxCursorPos : this.mousePos;
// seems ok so tell the app we scrolled
this[pos] = toAreaCoord(this.hovered.cursor[pos], this.hovered);
// redraw everything
app.draw();
}
// because we will hide it after a small time
scrollbars.willHide;
// called by the wheel event
scrollbars.scrollBy = function(deltaX, deltaY) {
// it's not coming from our scrollbars
this.hovered = null;
// we're moving horizontally
if (deltaX) {
var newLeft = this.left + deltaX;
// make sure we're in the bounds
this.left = newLeft > app.WIDTH - canvas.width ? app.WIDTH - canvas.width : newLeft < 0 ? 0 : newLeft;
// update the horizontal cursor
this.horizontal.cursor.left = toScrollCoords(this.left, this.horizontal);
// show our scrollbar
this.horizontal.visible = true;
}
if (deltaY) {
var newTop = this.top + deltaY;
this.top = newTop > app.HEIGHT - canvas.height ? app.HEIGHT - canvas.height : newTop < 0 ? 0 : newTop;
this.vertical.cursor.top = toScrollCoords(this.top, this.vertical);
this.vertical.visible = true;
}
// if we were called less than the required timeout
clearTimeout(this.willHide);
this.willHide = setTimeout(function() {
scrollbars.willHide = null;
scrollbars.hide();
app.draw();
}, 500);
// redraw everything
app.draw();
};
return scrollbars;
}();
var mousedown = function(e) {
// tell the browser we handle this
e.preventDefault();
// we're over one the scrollbars
if (app.scrollbars.hovered) {
// new promotion ! it becomes the dragged one
app.scrollbars.dragged = app.scrollbars.hovered;
app.scrollbars.scroll();
}
};
var mousemove = function(e) {
// check the coordinates of our canvas in the document
var rect = canvas.getBoundingClientRect();
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
// we're dragging something
if (app.scrollbars.dragged) {
// update the mouse position
app.scrollbars.mousePos = app.scrollbars.dragged.vertical ? y : x;
app.scrollbars.scroll();
} else if (app.scrollbars.isHover(x, y)) {
// something has changed, redraw to show or hide the scrollbar
app.draw();
}
e.preventDefault();
};
var mouseup = function() {
// we dropped it
app.scrollbars.dragged = null;
};
var mouseout = function() {
// we're out
if (app.scrollbars.visible()) {
app.scrollbars.hide();
app.scrollbars.dragged = false;
app.draw();
}
};
var mouseWheel = function(e) {
e.preventDefault();
app.scrollbars.scrollBy(e.deltaX, e.deltaY);
};
canvas.addEventListener('mousemove', mousemove);
canvas.addEventListener('mousedown', mousedown);
canvas.addEventListener('mouseup', mouseup);
canvas.addEventListener('mouseout', mouseout);
canvas.addEventListener('wheel', mouseWheel);
range.onchange = function() {
app.WIDTH = app.HEIGHT = this.value;
app.scrollbars.left = 0;
app.scrollbars.top = 0;
app.draw();
};
// an initial drawing
app.draw();
canvas {border: 1px solid;}
span{font-size: .8em;}
<canvas id="canvas" width="200" height="150"></canvas>
<span>
change the total area size
<input type="range" min="250" max="5000000" steps="250" value="5000" id="range" />
</span>
Main advantages :
no limitation for the size of your drawing areas.
you can customize your scrollbars as you wish.
you can control when the scrollbars are enable or not.
you can get the visible area quite easily.
Main caveats:
a bit more code than the CSS solution...
no really, that's a lot of code...
A third way I wrote some time ago for an other question took advantage of the ability to draw an other canvas with ctx.drawImage(). It has its own caveats and advantages, so I let you pick the one you need, but this last one also had a drag and slide feature which can be useful.
So your node drawings don't fit on the canvas size?
You can easily "shrink" your content to fit the visible canvas with just 1 command!
The context.scale(horizontalRescale,verticalRescale) command will shrink every following drawing by your specified horizontalRescale & verticalRescale percentages.
An Important note: You must make horizontalRescale,verticalRescale the same value or your content will be distorted.
The nice thing about using context.scale is that you don't have to change any of the code that draws your nodes ... canvas automatically scales all those nodes for you.
For example, this code will shrink your nodes to 80% of their original size:
var downscaleFactor= 0.80;
context.scale( downscaleFactor, downscaleFactor );
Rather than go through your 200+ lines of code, I leave it to you to calculate downscaleFactor.

Prevent map jumping/swapping hozirontally using Google Map API's groundoverlay

All,
I am new to Google Map API. I am trying to display some images over Google Maps. I want to use GroundOverlay because it is easier to implement animation effects later in my work.
The problem is the image will jump horizontally between 'world-tiles' especially when I drag the map crossing -180 deg and 180 deg. Only part of the screen shows the image, which is annoying. Here's a screenshot:
Does anybody know how to prevent this? Actually I would like to replicate the behavior at:
http://forevermore.net/articles/photo-zoom/
I guess if I can limit the left and right boundaries of the map, then the image will not warp horizontally. But the example showing above is based on the tile overlay not GroudOverlay. Any ideas to implement the behavior using GroundOverlay?
You can try using this:
google.maps.event.addListener(map,'center_changed',function() { checkBounds(); });
var allowedBounds = new google.maps.LatLngBounds(
new google.maps.LatLng( , ), //add your lat/long
new google.maps.LatLng( , ) //add your lat/long
);
function checkBounds() {
if(! allowedBounds.contains(map.getCenter())) {
var C = map.getCenter();
var X = C.lng();
var Y = C.lat();
var maxX = allowedBounds.getNorthEast().lng();
var maxY = allowedBounds.getNorthEast().lat();
var minX = allowedBounds.getSouthWest().lng();
var minY = allowedBounds.getSouthWest().lat();
if (X < minX) {X = minX;}
if (X > maxX) {X = maxX;}
if (Y < minY) {Y = minY;}
if (Y > maxY) {Y = maxY;}
map.setCenter(new google.maps.LatLng(Y,X));
}
}

How do I draw two parallel poly lines based on only two points (similar to google maps traffic)

I have a Polyline that is drawn from point A to point B, I'm trying to figure out how to draw two lines of a constant width that appear something like this image.
I only have two points, rather than four, I thought maybe there was a way to create four points, based on the two and then draw the corresponding lines, but I cant seem to get my head around it.
Any suggestions
I am currently trying to achieve the same thing, have two parallel polylines, but with different colors.
Have you seen this:
http://wtp2.appspot.com/ParallelLines.htm
It uses Google Maps v2 API, but it seems quite simple to convert it to API v3.
That's really complicated I think, because you have to re-calculate the route of the lines on every zoom-change.
When you don't need 2 different colors you could easy accomplish a similar effect by using the same route for two lines:
http://jsfiddle.net/doktormolle/KFsDB/
for google maps V3 if anyone is still interested
you can use this script which is modification on v2
// Parallel Polylines
// http://matthewschwartz.me/parallel-lines-and-google-maps-v3/
// Original Google Maps V2 awesome code by: Bill Chadwick March 2008
// Released as Free for any use # http://wtp2.appspot.com/ParallelLines.htm
//
// Modified for use with GMaps V3 by: Matthew Schwartz (schwartz.matthew#schwartzlink.net)
// Also released as free for any use
// Modified again for use by Jereme Causing. Fixed some errors and added parameters -June 2013
/*
* BDCCParallelLines(Array points<lat,lng>, String color, float weight, float opacity, float gap, String _type)
* _type can be either polyline or polygon. polyline is the default
* returns OverlayView();
*/
function BDCCParallelLines(points, color, weight, opacity, gapPx, _type, _editable) {
this.gapPx = gapPx;
this.points = points;
this.color = color;
this.weight = weight;
this.opacity = opacity;
this.prj = null;
this.line1 = null;
this.line2 = null;
this.zoomListener = null;
this._type = _type;
this._editable = false;
this.polygon = null;
if(_editable){
this._editable = _editable; //boolean
}
}
BDCCParallelLines.prototype = new google.maps.OverlayView();
// BDCCParallelLines implements the OverlayView interface
// Methods that need to be implemented in GMaps 3 = onAdd(), draw(), and onRemove()
BDCCParallelLines.prototype.onAdd = function() {
this.setProjection();
var foo = this;
var zoomRecalc = function() {
foo.onRemove();
foo.setProjection();
};
this.zoomListener = google.maps.event.addListener(this.map, 'zoom_changed', zoomRecalc);
}
BDCCParallelLines.prototype.setProjection = function() {
this.map = this.getMap();
var overlay = new google.maps.OverlayView();
overlay.draw = function() {};
overlay.setMap(this.map);
this.prj = overlay.getProjection();
}
BDCCParallelLines.prototype.onRemove = function() {
if(this.line2) {
this.line2.setMap(null);
this.line2 = null;
}
if(this.line1) {
this.line1.setMap(null);
this.line1 = null;
}
if (this.prj) {
this.prj = null;
}
if(this.zoomListener != null) {
google.maps.event.removeListener(this.zoomListener);
}
if(this.polygon){
this.polygon.setMap(null);
}
}
BDCCParallelLines.prototype.draw = function(map) {
if(this.line2) {
this.line2.setMap(null);
this.line2 = null;
}
if(this.line1) {
this.line1.setMap(null);
this.line1 = null;
}
this.recalc();
return;
}
BDCCParallelLines.prototype.redraw = function(force) {
return; //do nothing
}
BDCCParallelLines.prototype.recalc = function() {
// var zoom = this.map.getZoom();
var zoom = this.map.getZoom();
//left and right swapped throughout!
var pts1 = new Array();//left side of center
var pts2 = new Array();//right side of center
//shift the pts array away from the centre-line by half the gap + half the line width
var o = (this.gapPx + this.weight)/2;
var p2l,p2r;
for (var i=1; i<this.points.length; i++){
var p1lm1;
var p1rm1;
var p2lm1;
var p2rm1;
var thetam1;
var p1 = this.prj.fromLatLngToContainerPixel(this.points[i-1]);
var p2 = this.prj.fromLatLngToContainerPixel(this.points[i]);
var theta = Math.atan2(p1.x-p2.x,p1.y-p2.y) + (Math.PI/2);
var dl = Math.sqrt(((p1.x-p2.x)*(p1.x-p2.x))+((p1.y-p2.y)*(p1.y-p2.y)));
if(theta > Math.PI)
theta -= Math.PI*2;
var dx = Math.round(o * Math.sin(theta));
var dy = Math.round(o * Math.cos(theta));
var p1l = new google.maps.Point(p1.x+dx,p1.y+dy);
var p1r = new google.maps.Point(p1.x-dx,p1.y-dy);
p2l = new google.maps.Point(p2.x+dx,p2.y+dy);
p2r = new google.maps.Point(p2.x-dx,p2.y-dy);
if(i==1){ //first point
pts1.push(this.prj.fromContainerPixelToLatLng(p1l));
pts2.push(this.prj.fromContainerPixelToLatLng(p1r));
}
else{ // mid points
if(theta == thetam1){
// adjacent segments in a straight line
pts1.push(this.prj.fromContainerPixelToLatLng(p1l));
pts2.push(this.prj.fromContainerPixelToLatLng(p1r));
}
else{
var pli = this.intersect(p1lm1,p2lm1,p1l,p2l);
var pri = this.intersect(p1rm1,p2rm1,p1r,p2r);
var dlxi = (pli.x-p1.x);
var dlyi = (pli.y-p1.y);
var drxi = (pri.x-p1.x);
var dryi = (pri.y-p1.y);
var di = Math.sqrt((drxi*drxi)+(dryi*dryi));
var s = o / di;
var dTheta = theta - thetam1;
if(dTheta < (Math.PI*2))
dTheta += Math.PI*2;
if(dTheta > (Math.PI*2))
dTheta -= Math.PI*2;
if(dTheta < Math.PI){
//intersect point on outside bend
pts1.push(this.prj.fromContainerPixelToLatLng(p2lm1));
pts1.push(this.prj.fromContainerPixelToLatLng(new google.maps.Point(p1.x+(s*dlxi),p1.y+(s*dlyi)),zoom));
pts1.push(this.prj.fromContainerPixelToLatLng(p1l));
}
else if (di < dl){
pts1.push(this.prj.fromContainerPixelToLatLng(pli));
}
else{
pts1.push(this.prj.fromContainerPixelToLatLng(p2lm1));
pts1.push(this.prj.fromContainerPixelToLatLng(p1l));
}
var dxi = (pri.x-p1.x)*(pri.x-p1.x);
var dyi = (pri.y-p1.y)*(pri.y-p1.y);
if(dTheta > Math.PI){
//intersect point on outside bend
pts2.push(this.prj.fromContainerPixelToLatLng(p2rm1));
pts2.push(this.prj.fromContainerPixelToLatLng(new google.maps.Point(p1.x+(s*drxi),p1.y+(s*dryi)),zoom));
pts2.push(this.prj.fromContainerPixelToLatLng(p1r));
}
else if(di<dl)
pts2.push(this.prj.fromContainerPixelToLatLng(pri));
else{
pts2.push(this.prj.fromContainerPixelToLatLng(p2rm1));
pts2.push(this.prj.fromContainerPixelToLatLng(p1r));
}
}
}
p1lm1 = p1l;
p1rm1 = p1r;
p2lm1 = p2l;
p2rm1 = p2r;
thetam1 = theta;
}
if(this._type == 'polyline' || this._type == null) //default
{
pts1.push(this.prj.fromContainerPixelToLatLng(p2l));//final point
pts2.push(this.prj.fromContainerPixelToLatLng(p2r));
if(this.line1)
this.line1.setMap(null);
this.line1 = new google.maps.Polyline({
path: pts1,
strokeColor: this.color,
strokeOpacity: this.opacity,
strokeWeight: this.weight,
editable: this._editable
});
this.line1.setMap(this.map);
if(this.line2)
this.line1.setMap(null);
this.line2 = new google.maps.Polyline({
path: pts2,
strokeColor: this.color,
strokeOpacity: this.opacity,
strokeWeight: this.weight
});
this.line2.setMap(this.map);
}else if(this._type == 'polygon' ){
pts1.push(this.prj.fromContainerPixelToLatLng(p2l));//final point
pts2.push(this.prj.fromContainerPixelToLatLng(p2r));
var newpts = pts1.concat(pts2.reverse());
if(this.polygon){
this.polygon.setMap(null);
}
this.polygon = new google.maps.Polygon({
paths: newpts,
editable: true,
strokeColor: this.color,
fillColor: this.color,
strokeOpacity: this.opacity,
strokeWeight: this.weight,
editable: this._editable
});
this.polygon.setMap(this.map);
/* if(this.line2)
this.line1.setMap(null);
this.line2 = new google.maps.Polyline({
path: pts2,
strokeColor: this.color,
strokeOpacity: this.opacity,
strokeWeight: this.weight
});
this.line2.setMap(this.map);
*/
}
}
BDCCParallelLines.prototype.intersect = function(p0,p1,p2,p3)
{
// this function computes the intersection of the sent lines p0-p1 and p2-p3
// and returns the intersection point,
var a1,b1,c1, // constants of linear equations
a2,b2,c2,
det_inv, // the inverse of the determinant of the coefficient matrix
m1,m2; // the slopes of each line
var x0 = p0.x;
var y0 = p0.y;
var x1 = p1.x;
var y1 = p1.y;
var x2 = p2.x;
var y2 = p2.y;
var x3 = p3.x;
var y3 = p3.y;
// compute slopes, note the cludge for infinity, however, this will
// be close enough
if ((x1-x0)!==0)
m1 = (y1-y0)/(x1-x0);
else
m1 = 1e+10; // close enough to infinity
if ((x3-x2)!==0)
m2 = (y3-y2)/(x3-x2);
else
m2 = 1e+10; // close enough to infinity
// compute constants
a1 = m1;
a2 = m2;
b1 = -1;
b2 = -1;
c1 = (y0-m1*x0);
c2 = (y2-m2*x2);
// compute the inverse of the determinate
det_inv = 1/(a1*b2 - a2*b1);
// use Kramers rule to compute xi and yi
var xi=((b1*c2 - b2*c1)*det_inv);
var yi=((a2*c1 - a1*c2)*det_inv);
return new google.maps.Point(Math.round(xi),Math.round(yi));
}
as fount on this on this jsfiddle parrallel drawing

Google Maps specific part

I am working on Google maps V3 and i want to get a specific part of Google map like i want to display only Pakistan on Google map and other map should hide completely .?
You probably can't hide other map parts completely but you can set boundaries to the map so that the user cannot scroll any further than the defined point
google.maps.event.addListener(map,'center_changed',function() { checkBounds(); });
function checkBounds() {
if(! allowedBounds.contains(map.getCenter())) {
var C = map.getCenter();
var X = C.lng();
var Y = C.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));
}
}

Setting maximum and minimum bounds using Google Maps API

Was wondering if someone could provide an example of how to set maximum and minimum extents using GMaps2? In OpenLayers, here's how it is done:
var point1 = new OpenLayers.Geometry.Point(7, 48);
point1.transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));
var point2 = new OpenLayers.Geometry.Point(11, 54);
point2.transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));
var bounds = new OpenLayers.Bounds();
bounds.extend(point1);
bounds.extend(point2);
bounds.toBBOX();
map = new OpenLayers.Map("map", {
maxExtent: bounds,
maxResolution:"auto",
maxZoomLevel: 8,
projection:"EPSG:900913",
controls: []
});
map.addLayer(new OpenLayers.Layer.OSM.Osmarender("Osmarender"));
map.zoomToMaxExtent();
//map.zoomToExtent(bounds);
What would be the GMaps2 equivalent?
The following is the Google Maps API 2.x equivalent to limit the panning of the map to predefined bounds:
// Bounds for North America
var allowedBounds = new GLatLngBounds(new GLatLng(48.197218, -127.529297),
new GLatLng(28.72913, -68.818359));
function checkBounds() {
if (allowedBounds.contains(map.getCenter())) {
return;
}
var c = map.getCenter();
var x = c.lng();
var y = c.lat();
var maxX = allowedBounds.getNorthEast().lng();
var maxY = allowedBounds.getNorthEast().lat();
var minX = allowedBounds.getSouthWest().lng();
var minY = allowedBounds.getSouthWest().lat();
if (x < minX) {x = minX;}
if (x > maxX) {x = maxX;}
if (y < minY) {y = minY;}
if (y > maxY) {y = maxY;}
map.setCenter(new GLatLng(y, x));
}
GEvent.addListener(map, "move", function() { checkBounds(); });
Thanks for getting back to me Chris and Daniel. What I meant by 'extents' is that when zoomed out is maximized, the world map doesn't tile. I don't want to see 2, 3 or even 4 North America's and other continents. The world is big enough with just one. When I zoom in, I want to set a limit to how far one can zoom in to account for our data resolution. Zoom in too far returns results outside our data resolution. I did manage to find an acceptable solution by restricting the zoom levels. Anyway, FWIW, here's what I came up. It's a bit less verbose but isn't doing exactly what Daniel does in his code:
//===== Restrict Zoom Levels =====
var mapTypes = map.getMapTypes();
// Overwrite the getMinimumResolution() and getMaximumResolution() methods
for (var i=0; i<mapTypes.length; i++) {
mapTypes[i].getMinimumResolution = function() {return 2;}
mapTypes[i].getMaximumResolution = function() {return 9;}
}
I still want to try Daniel's code to see what it does. Special thanks to you for taking the time to write this up Dan.
-=Al