I am working on a sort of itinerary mapper for packaged vacations, and I'm really happy with what I've done so far; I have the directions api implemented with a custom renderer, so I can take driving directions, and plot my own polyline complete with directional arrows that aren't google's awful ones spaced along the path. I am not exactly a math expert, and I am trying to figure out how I could make a path parallel to another path. For example, the itinerary goes from city 1 to city 2, and then back to city 1.
I want to offset the trip back to city 1's polyline, so that it mirrors the path, but travels parallel to it. Ideally, I would like to when I create the path, check for intersecting points in other paths, and if any are found, offset the path at those points only. This would be a better implementation, because you could for instance parallel the path only where it happens to intersect another one, like when it meets another path only for a short time.
I found this code for API2 from bill chadwick
The link is here: http://wtp2.appspot.com/ParallelLines.htm
Update: Somehow managed to convert this old v2 script to get it working in v3, but I'm experiencing some troubles...
It is more than doubling the original number of points, and following the path, but really throwing them in randomly. Screenshot here:
The class I converted is here:
function BDCCParallelLines(points, color, weight, opacity, opts, gapPx) {
console.log('Pllel COnstructor Initialized');
this.gapPx = gapPx;
this.points = points;
this.color = color;
this.weight = weight;
this.opacity = opacity;
this.opts = opts;
this.line1 = null;
this.line2 = null;
this.lstnZoom = null;
}
BDCCParallelLines.prototype = new google.maps.OverlayView();
BDCCParallelLines.prototype.onAdd = function() {
console.log('Pllel Initialized');
this.prj = map.getProjection();
var self = this;
this.lstnZoom = google.maps.event.addListener(map, "zoom_changed", function() {
self.recalc();
});
this.recalc();//first draw
}
BDCCParallelLines.prototype.onRemove = function() {
if(this.line2)
this.line2.setMap(null);
if(this.line1)
this.line1.setMap(null);
if(this.lstnZoom != null)
google.maps.event.removeListener(this.lstnZoom);
}
BDCCParallelLines.prototype.copy = function() {
return new BDCCParallelLines(this.points,this.color,this.weight,this.opacity,this.opts,this.gapPx);
}
BDCCParallelLines.prototype.draw = function(force) {
return; //do nothing
}
/**
* #param {google.maps.Map} map
* #param {google.maps.LatLng} latlng
* #param {int} z
* #return {google.maps.Point}
*/
BDCCParallelLines.prototype.latLngToPoint = function(latlng, z){
var normalizedPoint = map.getProjection().fromLatLngToPoint(latlng); // returns x,y normalized to 0~255
var scale = Math.pow(2, z);
var pixelCoordinate = new google.maps.Point(normalizedPoint.x * scale, normalizedPoint.y * scale);
return pixelCoordinate;
};
/**
* #param {google.maps.Map} map
* #param {google.maps.Point} point
* #param {int} z
* #return {google.maps.LatLng}
*/
BDCCParallelLines.prototype.pointToLatlng = function(point, z){
var scale = Math.pow(2, z);
var normalizedPoint = new google.maps.Point(point.x / scale, point.y / scale);
var latlng = map.getProjection().fromPointToLatLng(normalizedPoint);
return latlng;
};
BDCCParallelLines.prototype.recalc = function() {
var distallowance;
console.log('recalc called');
var zoom = map.getZoom();
distallowance = 1.6;
if(zoom > 6){
distallowance = 1.3;
if(zoom > 9){
distallowance = .7;
if( zoom > 13){
distallowance = .2;
if( zoom > 15){
distallowance = .0001;
}
}
}
}
console.log('Zoom Level: ' + zoom);
console.log('Allowance = ' + distallowance);
var pts1 = new Array();//left 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.latLngToPoint(this.points[i-1], zoom)
var p2 = this.latLngToPoint(this.points[i], zoom)
var theta = Math.atan2(p1.x-p2.x,p1.y-p2.y);
theta = theta + (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.pointToLatlng(p1l,zoom));
}
else{ // mid this.points
if(distbetweentwo(this.points[i-1], this.points[i]) > distallowance){
if(theta == thetam1){
// adjacent segments in a straight line
pts1.push(this.pointToLatlng(p1l,zoom));
}
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.pointToLatlng(p2lm1,zoom));
pts1.push(this.pointToLatlng(new google.maps.Point(p1.x+(s*dlxi),p1.y+(s*dlyi)),zoom));
pts1.push(this.pointToLatlng(p1l,zoom));
}
else if (di < dl){
pts1.push(this.pointToLatlng(pli,zoom));
}
else{
pts1.push(this.pointToLatlng(p2lm1,zoom));
pts1.push(this.pointToLatlng(p1l,zoom));
}
}
}
else{
//console.log(distbetweentwo(this.points[i-1], this.points[i]));
}
}
p1lm1 = p1l;
p1rm1 = p1r;
p2lm1 = p2l;
p2rm1 = p2r;
thetam1 = theta;
//end loop
}
pts1.push(this.pointToLatlng(p2l,zoom));//final point
// console.log(pts1);
if(this.line1)
this.line1.setMap(null);
this.line1 = new google.maps.Polyline({
strokeColor: this.color,
strokeOpacity: this.opacity,
strokeWeight: this.weight,
map: map,
path: pts1 });
this.line1.setMap(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));
}
This is working to a point... It is working as well as the original implementation. The entire path is recalculated on a zoom basis, and I kind of hacked the function to skip very short paths(weird angles) at higher zoom levels, it more closely follows the path the more you zoom in.
I would rather just have a fixed distance offset that is not recalculated, as it is pretty intensive... There are many programs which accomplish this feat, rhino3d, autocad, illustrator... I feel like it would be great for driving directions for google maps itself, an offsetting of the path so you can distinguish the return trip and the original trip.
If anybody has done anything similar to this in JS even if its not for google maps specifically, I would love to see it. Links I am investigating:
http://processingjs.nihongoresources.com/bezierinfo/
http://www.groupsrv.com/computers/about21532.html
Offsetting paths in general is a pretty tricky buisness. This paper (scientific paper alert) gives a good description of the steps taken for 'professional' offset algorithms.
http://cgcad.thss.tsinghua.edu.cn/~yongjh/papers/CiI2007V58N03P0240.pdf
You don't seem to want anything as fancy as in the demo. From what I gather you just want the same polyline, only shifted some pixels to the right and maybe some to the top so it doesn't overlap.
The code you posted has a latLngToPoint function and pointToLatLng function. I think the directions you get from Google are LatLng, so you can convert those to Points, increase the x and y property, and convert it back to a LatLng and draw your Polyline.
This should get you a line that exactly follows the original line. But, it wont look as fancy as the one in the demo. Since it wont be adding any points to smooth the line.
Related
I am trying to make my own raytracer in Javascript. So far the spheres work very well. I now want to expand the capabilities to include triangles, and from there I can go to squares, cubes and beyond. The code I have for finding intersections with triangles is as follows
function triangleIntersection(t, r) {
var norm = triangleNormal(t);
var dist = triangleDistance(t);
var a = Vector.dotProduct(r.vector, norm);
if (a === 0) {
return -1;
} else {
var b = Vector.dotProduct(norm, Vector.add(r.point, Vector.negative(Vector.multiply(norm, dist))));
var d = -1 * b / a;
var qx = Vector.scale(r.vector, d).x + r.point.x;
var qy = Vector.scale(r.vector, d).y + r.point.y;
var qz = Vector.scale(r.vector, d).z + r.point.z;
var q = new Vector(qx, qy, qz);
var ca = Vector.subtract(t.points[2], t.points[0]);
var qa = Vector.subtract(q, t.points[0]);
var t1 = Vector.dotProduct(Vector.crossProduct(ca, qa), norm);
var bc = Vector.subtract(t.points[1], t.points[2]);
var qc = Vector.subtract(q, t.points[2]);
var t2 = Vector.dotProduct(Vector.crossProduct(bc, qc), norm);
var ab = Vector.subtract(t.points[0], t.points[1]);
var qb = Vector.subtract(q, t.points[1]);
var t3 = Vector.dotProduct(Vector.crossProduct(ab, qb), norm);
if ((t1 >= 0) && (t2 >= 0) && (t3 >= 0)) {
return 1 * b / a;
} else {
return -1;
}
}
}
Triangle objects have a point array (points[]) and item 0 is point A, item 1 is point B and item 2 is point C. The parameter t is one of these triangles. The parameter r is a ray object, with properties point which is the origin, and vector, which is the direction.
I also have these functions for finding normal and distance of a triangle.
function triangleNormal(s) {
var ca = Vector.subtract(s.points[2], s.points[0]);
var ba = Vector.subtract(s.points[1], s.points[0]);
var norm = Vector.unitVector(Vector.crossProduct(ca, ba));
return norm;
}
function triangleDistance(t) {
return Vector.dotProduct(triangleNormal(t, 0), t.points[0]);
}
When I render my scene the triangle I use in my scene is red colored. No matter how far I move my camera back the triangle fills the whole scene red. I do not know why this happens.
An important error in your plane intersection code is this comparison operation:
a === 0
There are two things wrong with it:
For ray tracing you want the ray to hit planes in front of its source, not behind it, so you need a < 0.
Even if you did want the ray to hit planes behind it, you must never do equality operations between floating point values, because floating point calculations are not exact. (You must do something like abs(a) < 1e-6f or some small value)
I am trying to write a script to place 100 circles of varying sizes onto a stage. I've outlined the concise requirements below.
Given the following:
var stage; // contains a "width" and "height" property.
var circle; // the circle class. contains x, y, radius & a unique id property.
var circleArray; // contains 100 circle instances
requirements:
write a function to place 100 circles of varying radius onto the stage.
placements must be random but evenly distributed (no clumping).
placement must be performant - this will be executing on a mobile web browser.
circles must not intersect/overlap other circles.
circle.x >= 0 must be true.
circle.y >= 0 && circle.y <= stage.height must be true.
circles may have any of the following radius sizes (assigned at creation):
150
120
90
80
65
My current attempt is a brute-force method, which does not operate efficiently. If I attempt to insert any more than ~10 circles, the browser hangs. Below is my current implementation, which I am completely OK with throwing away in favor of a more performant / better one.
Here is a live demo (NOTE: there is no actual drawing code, just the logic, but it will still lock up the browser so be warned!!) http://jsbin.com/muhiziduxu/2/edit?js,console
function adjustForOverlap (circleArray) {
// a reference to the circle that is invoking this function.
var _this = this;
// remove this circle from the array we are iterating over.
var arr = circleArray.filter(function (circle){
return circle.id !== _this.id;
});
// while repeat == true, the circle may be overlapping something.
var repeat = true;
while(repeat) {
var hasOverlap = false;
for (var i=0; i<arr.length; i++) {
var other = arr[i];
var dx = _self.x - other.x;
var dy = _self.y - other.y;
var rr = _self.radius + other.radius;
if (dx * dx + dy * dy < rr * rr) {
// if here, then an overlap was detected.
hit = true;
break;
}
}
// if hit is false, the circle didn't overlap anything, so break.
if (hit === false) {
repeat = false;
break;
} else {
// an overlap was detected, so randomize position.
_self.x = Math.random() * (stage.width*2);
_self.y = Math.random() * stage.height;
}
}
}
There are lots of efficient collision detection algorithms. Many of them work by dividing up the space into cells and maintaining a separate data structure with efficient lookup of other objects in the cell. The basic steps are:
Identify a random spot for your new circle
Determine which cells it's in
Look in each of those cells for a collision
If there's a collision, goto 1.
Else, add the new circle to each of the cells it overlaps.
You can use a simple square grid (i.e. a 2-d array) for the cell data structure, or something else like a quadtree. You can also in some cases get a bit of extra speed by trying a cheap-but-coarse collision check first (do the bounding boxes overlap), and if that returns true try the slightly more expensive and exact check.
Update
For quadtrees, check out d3-quadtree, which ought to give you a pretty good implementation, with examples.
For a (very quick, untested) 2-d array implementation:
function Grid(radius, width, height) {
// I'm not sure offhand how to find the optimum grid size.
// Let's use a radius as a starting point
this.gridX = Math.ceil(width / radius);
this.gridY = Math.ceil(height / radius);
// Determine cell size
this.cellWidth = width / this.gridX;
this.cellHeight = height / this.gridY;
// Create the grid structure
this.grid = [];
for (var i = 0; i < gridY; i++) {
// grid row
this.grid[i] = [];
for (var j = 0; j < gridX; j++) {
// Grid cell, holds refs to all circles
this.grid[i][j] = [];
}
}
}
Grid.prototype = {
// Return all cells the circle intersects. Each cell is an array
getCells: function(circle) {
var cells = [];
var grid = this.grid;
// For simplicity, just intersect the bounding boxes
var gridX1Index = Math.floor(
(circle.x - circle.radius) / this.cellWidth
);
var gridX2Index = Math.ceil(
(circle.x + circle.radius) / this.cellWidth
);
var gridY1Index = Math.floor(
(circle.y - circle.radius) / this.cellHeight
);
var gridY2Index = Math.ceil(
(circle.y + circle.radius) / this.cellHeight
);
for (var i = gridY1Index; i < gridY2Index; i++) {
for (var j = gridX1Index; j < gridX2Index; j++) {
// Add cell to list
cells.push(grid[i][j]);
}
}
return cells;
},
add: function(circle) {
this.getCells(circle).forEach(function(cell) {
cell.push(circle);
});
},
hasCollisions: function(circle) {
return this.getCells(circle).some(function(cell) {
return cell.some(function(other) {
return this.collides(circle, other);
}, this);
}, this);
},
collides: function (circle, other) {
if (circle === other) {
return false;
}
var dx = circle.x - other.x;
var dy = circle.y - other.y;
var rr = circle.radius + other.radius;
return (dx * dx + dy * dy < rr * rr);
}
};
var g = new Grid(150, 1000, 800);
g.add({x: 100, y: 100, radius: 50});
g.hasCollisions({x: 100, y:80, radius: 100});
Here's a fully-functional example: http://jsbin.com/cojoxoxufu/1/edit?js,output
Note that this only shows 30 circles. It looks like the problem is often unsolvable with your current radii, width, and height. This is set up to look for up to 500 locations for each circle before giving up and accepting a collision.
Please bear with me. I appreciate any pointers / patience and help in understanding the code pasted below.
All the lines marked with the '// ??' comment is where I don't have a clue what's happening.
I picked up the Building an HTML5 Game book (http://buildanhtml5game.com/) in the hopes of learning how to write a game, more especially in HTML5 and JavaScript.
To my surprise, the book does not really explain various background information or assumes the readers already know their way around.
I am not a mathematics expert and that's one big reason I am stuck with the code below. The book barely touches the surface and the explanation is in a line or two (leaving me more confused).
Please, I may ask, do provide me with links so I study the mathematics behind the formulas below and write some English explanation as well so I understand.
I don't want to skip the mathematics parts in this book because it defeats the purpose.
Much appreciated.
var BubbleShoot = window.BubbleShoot || {};
BubbleShoot.CollisionDetector = (function($){
var CollisionDetector = {
findIntersection : function(curBubble,board,angle) {
// 'angle' is the angle between the 'curBubble' starting bubble
// at the center of the screen and the mouse click's direction aimed at the bubbles
// above.
// I understand how that angle is computed in previous code snippets.
// 'board' contains an array of bubble objects that are static and laid up on the board
// waiting to be shot at with 'curbubble' and 'angle'.
// Get the bubble rows container
var rows = board.getRows();
var collision = null;
// Get the bubble's that will be shot from the center coordinates
var pos = curBubble.getSprite().position();
// The start coordinates are the bubble's center
var start = {
left : pos.left + BubbleShoot.ui.BUBBLE_DIMS/2,
top : pos.top + BubbleShoot.ui.BUBBLE_DIMS/2
};
// ?? (A) what does this give us (mathematically speaking, what are those variables called)
var dx = Math.sin(angle);
var dy = -Math.cos(angle);
for(var i=0;i<rows.length;i++){
// for each row
var row = rows[i];
for(var j=0;j<row.length;j++){
// Get bubble at i,j
var bubble = row[j];
if(bubble){
// Get bubble's coordinates. we want to see if the starting bubble
// will collidate with this static bubble if the starting bubble was fired
// with an angle 'angle'
var coords = bubble.getCoords();
// ?? (B) that's not the distance between the two points!
// ?? what do you call this mathematically?
var distToBubble = {
x : start.left - coords.left,
y : start.top - coords.top
};
// ?? (C) what is 't'? what does it give? can you please give me reference
// ?? so I study the proper math behind its purpose?
var t = dx * distToBubble.x + dy * distToBubble.y;
// ?? (D) why '-t' * dx and what does this give us?
var ex = -t * dx + start.left;
// ?? (E) same for 'ey'
var ey = -t * dy + start.top;
// distance between the two points (ex,ey) and (coords.left,coords.top)
// ?? (F) I am not sure what are 'ex' and 'ey'
var distEC = Math.sqrt((ex - coords.left) * (ex - coords.left) +
(ey - coords.top) * (ey - coords.top));
if(distEC<BubbleShoot.ui.BUBBLE_DIMS * .75){
// ?? (G) dt is what?
var dt = Math.sqrt(BubbleShoot.ui.BUBBLE_DIMS * BubbleShoot.
ui.BUBBLE_DIMS - distEC * distEC);
// ?? (H) what's this?
var offset1 = {
x : (t - dt) * dx,
y : -(t - dt) * dy
};
// ?? (I) and this?!
var offset2 = {
x : (t + dt) * dx,
y : -(t + dt) * dy
};
// ?? (J) distance between something
var distToCollision1 = Math.sqrt(offset1.x * offset1.x +
offset1.y * offset1.y);
var distToCollision2 = Math.sqrt(offset2.x * offset2.x +
offset2.y * offset2.y);
if(distToCollision1 < distToCollision2){
var distToCollision = distToCollision1;
var dest = {
x : offset1.x + start.left,
y : offset1.y + start.top
};
}else{
var distToCollision = distToCollision2;
var dest = {
x : -offset2.x + start.left,
y : offset2.y + start.top
};
}
if(!collision || collision.distToCollision>distToCollision){
collision = {
bubble : bubble,
distToCollision : distToCollision,
coords : dest
};
};
};
};
};
};
return collision;
}
};
return CollisionDetector;
})(jQuery);
You can play the game here just to see how it looks: http://buildanhtml5game.com/?page_id=20
I am trying to get the id of the shape my mouse is currently hovering over.
my shapes are in a container
// creating the layers
gridLayer = new PIXI.DisplayObjectContainer ();
gridLayer.setInteractive(true);
stage.addChild(gridLayer);
and i am creating each shape like this;
function drawHexagon(x,y, size, gap,scale, color, iterI, iterJ, type) {
var shape = new PIXI.Graphics();
// set a fill and line style
shape.beginFill(color);
shape.lineStyle(1, 0xa0a0a0, 1);
size = size-gap;
for (i = 0; i < 7; i++) {
angle = 2 * Math.PI / 6 * (i + 0.5);
var x_i = x + size * Math.cos(angle);
var y_i = y + size * Math.sin(angle);
if (i === 0) {
shape.moveTo(x_i, scale *y_i)
}
else {
shape.lineTo(x_i, scale * y_i)
}
};
shape.endFill();
// calculate and save the axial coordinates
var cX = iterJ - (iterI - (iterI&1)) / 2;
var cZ = iterI;
var cY = -1*(cX+cZ);
shape.hexId = cX + "x" + cY + "y" + cZ + "z";
shape.hexPosX = x;
shape.hexPosY = y;
shape.setInteractive(true);
shape.mouseover = function(mouseData){
console.log("MOUSE OVER " + shape.hexId);
}
shape.click = function(mouseData){
console.log("MOUSE CLICK " + shape.hexId);
}
gridLayer.addChild(shape);
}
However, clicking on any shape or hovering over it is not showing me anything in the console. what am i doing wrong?
i have tried both
shape.setInteractive(true)
and
shape.interactive = true
but neither seems to work for me.
EDIT: i have added a jsfiddle. it doesnt works (i dont know how to link things in jsfiddle) but you can see my entire code in there.
http://jsfiddle.net/9aqHz/1/
For a PIXI.Graphics object to be interactive you need to set a hitArea shape (it can be a Rectangle, Circle or a Polygon):
shape.hitArea = new PIXI.Polygon([
new PIXI.Point(/* first point */),
new PIXI.Point(/* second point */),
new PIXI.Point(/* third point */),
new PIXI.Point(/* fourth point */),
new PIXI.Point(/* fifth point */)
]);
Another approach would be to generate a texture from the shape and use a Sprite, but the hit area would be the entire rectangular bounds of the hexagon:
var texture = shape.generateTexture();
var sprite = new PIXI.Sprite(texture);
sprite.setInteractive(true);
sprite.anchor.set(0.5, 0.5);
Fiddle with this applied to your example
#imcg I updated your code so it workes with Pixi 3.0.8
- sprite.setInteractive(true);
+ shape.interactive = true;
+ shape.buttonMode = true;
- sprite.setInteractive(true)
+ sprite.interactive = true;
+ sprite.buttonMode = true;
http://jsfiddle.net/LP2j8/56/
I will add a bit of info for anyone who is in the same boat i was in;
When you define a shape as a geom, you have to explicitly state a hitarea.
So adding the following code makes it work;
shape.hitArea = new PIXI.Polygon(vertices);
shape.interactive = true;
shape.click = function(mouseData){
console.log("MOUSE CLICK " + shape.hexId);
}
But, when you define a shape as a sprite/texture, you dont need to do this.
in cases of sprites, just setting shape.interactive = true for the sprite is sufficient. You dont need to set the interactive property for the parent object or the stage.
I am trying to create a Google Map where the user can plot the route he walked/ran/bicycled and see how long he ran. The GPolyline class with it’s getLength() method is very helpful in this regard (at least for Google Maps API V2), but I wanted to add markers based on distance, for example a marker for 1 km, 5 km, 10 km, etc., but it seems that there is no obvious way to find a point on a polyline based on how far along the line it is. Any suggestions?
Having answered a similar problem a couple of months ago on how to tackle this on the server-side in SQL Server 2008, I am porting the same algorithm to JavaScript using the Google Maps API v2.
For the sake of this example, let's use a simple 4-point polyline, with a total length of circa 8,800 meters. The snippet below will define this polyline and will render it on the map:
var map = new GMap2(document.getElementById('map_canvas'));
var points = [
new GLatLng(47.656, -122.360),
new GLatLng(47.656, -122.343),
new GLatLng(47.690, -122.310),
new GLatLng(47.690, -122.270)
];
var polyline = new GPolyline(points, '#f00', 6);
map.setCenter(new GLatLng(47.676, -122.343), 12);
map.addOverlay(polyline);
Now before we approach the actual algorithm, we will need a function that returns the destination point when given a start point, an end point, and the distance to travel along that line, Luckily, there are a few handy JavaScript implementations by Chris Veness at Calculate distance, bearing and more between Latitude/Longitude points.
In particular I have adapted the following two methods from the above source to work with Google's GLatLng class:
Destination point given distance and bearing from start point
Bearing
These were used to extend Google's GLatLng class with a method moveTowards(), which when given another point and a distance in meters, it will return another GLatLng along that line when the distance is travelled from the original point towards the point passed as a parameter.
GLatLng.prototype.moveTowards = function(point, distance) {
var lat1 = this.lat().toRad();
var lon1 = this.lng().toRad();
var lat2 = point.lat().toRad();
var lon2 = point.lng().toRad();
var dLon = (point.lng() - this.lng()).toRad();
// Find the bearing from this point to the next.
var brng = Math.atan2(Math.sin(dLon) * Math.cos(lat2),
Math.cos(lat1) * Math.sin(lat2) -
Math.sin(lat1) * Math.cos(lat2) *
Math.cos(dLon));
var angDist = distance / 6371000; // Earth's radius.
// Calculate the destination point, given the source and bearing.
lat2 = Math.asin(Math.sin(lat1) * Math.cos(angDist) +
Math.cos(lat1) * Math.sin(angDist) *
Math.cos(brng));
lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(angDist) *
Math.cos(lat1),
Math.cos(angDist) - Math.sin(lat1) *
Math.sin(lat2));
if (isNaN(lat2) || isNaN(lon2)) return null;
return new GLatLng(lat2.toDeg(), lon2.toDeg());
}
Having this method, we can now tackle the problem as follows:
Iterate through each point of the path.
Find the distance between the current point in the iteration to the next point.
If the distance in point 2 is greater the distance we need to travel on the path:
...then the destination point is between this point and the next. Simply apply the moveTowards() method to the current point, passing the next point and the distance to travel. Return the result and break the iteration.
Else:
...the destination point is further in the path from the next point in the iteration. We need to subtract the distance between this point and the next point from the total distance to travel along the path. Continue through the iteration with the modified distance.
You may have noticed that we can easily implement the above recursively, instead of iteratively. So let's do it:
function moveAlongPath(points, distance, index) {
index = index || 0; // Set index to 0 by default.
if (index < points.length) {
// There is still at least one point further from this point.
// Construct a GPolyline to use its getLength() method.
var polyline = new GPolyline([points[index], points[index + 1]]);
// Get the distance from this point to the next point in the polyline.
var distanceToNextPoint = polyline.getLength();
if (distance <= distanceToNextPoint) {
// distanceToNextPoint is within this point and the next.
// Return the destination point with moveTowards().
return points[index].moveTowards(points[index + 1], distance);
}
else {
// The destination is further from the next point. Subtract
// distanceToNextPoint from distance and continue recursively.
return moveAlongPath(points,
distance - distanceToNextPoint,
index + 1);
}
}
else {
// There are no further points. The distance exceeds the length
// of the full path. Return null.
return null;
}
}
With the above method, if we define an array of GLatLng points, and we invoke our moveAlongPath() function with this array of points and with a distance of 2,500 meters, it will return a GLatLng on that path at 2.5km from the first point.
var points = [
new GLatLng(47.656, -122.360),
new GLatLng(47.656, -122.343),
new GLatLng(47.690, -122.310),
new GLatLng(47.690, -122.270)
];
var destinationPointOnPath = moveAlongPath(points, 2500);
// destinationPointOnPath will be a GLatLng on the path
// at 2.5km from the start.
Therefore all we need to do is to call moveAlongPath() for each check point we need on the path. If you need three markers at 1km, 5km and 10km, you can simply do:
map.addOverlay(new GMarker(moveAlongPath(points, 1000)));
map.addOverlay(new GMarker(moveAlongPath(points, 5000)));
map.addOverlay(new GMarker(moveAlongPath(points, 10000)));
Note however that moveAlongPath() may return null if we request a check point further from the total length of the path, so it will be wiser to check for the return value before passing it to new GMarker().
We can put this together for the full implementation. In this example we are dropping a marker every 1,000 meters along the 8.8km path defined earlier:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<title>Google Maps - Moving point along a path</title>
<script src="http://maps.google.com/maps?file=api&v=2&sensor=false"
type="text/javascript"></script>
</head>
<body onunload="GUnload()">
<div id="map_canvas" style="width: 500px; height: 300px;"></div>
<script type="text/javascript">
Number.prototype.toRad = function() {
return this * Math.PI / 180;
}
Number.prototype.toDeg = function() {
return this * 180 / Math.PI;
}
GLatLng.prototype.moveTowards = function(point, distance) {
var lat1 = this.lat().toRad();
var lon1 = this.lng().toRad();
var lat2 = point.lat().toRad();
var lon2 = point.lng().toRad();
var dLon = (point.lng() - this.lng()).toRad();
// Find the bearing from this point to the next.
var brng = Math.atan2(Math.sin(dLon) * Math.cos(lat2),
Math.cos(lat1) * Math.sin(lat2) -
Math.sin(lat1) * Math.cos(lat2) *
Math.cos(dLon));
var angDist = distance / 6371000; // Earth's radius.
// Calculate the destination point, given the source and bearing.
lat2 = Math.asin(Math.sin(lat1) * Math.cos(angDist) +
Math.cos(lat1) * Math.sin(angDist) *
Math.cos(brng));
lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(angDist) *
Math.cos(lat1),
Math.cos(angDist) - Math.sin(lat1) *
Math.sin(lat2));
if (isNaN(lat2) || isNaN(lon2)) return null;
return new GLatLng(lat2.toDeg(), lon2.toDeg());
}
function moveAlongPath(points, distance, index) {
index = index || 0; // Set index to 0 by default.
if (index < points.length) {
// There is still at least one point further from this point.
// Construct a GPolyline to use the getLength() method.
var polyline = new GPolyline([points[index], points[index + 1]]);
// Get the distance from this point to the next point in the polyline.
var distanceToNextPoint = polyline.getLength();
if (distance <= distanceToNextPoint) {
// distanceToNextPoint is within this point and the next.
// Return the destination point with moveTowards().
return points[index].moveTowards(points[index + 1], distance);
}
else {
// The destination is further from the next point. Subtract
// distanceToNextPoint from distance and continue recursively.
return moveAlongPath(points,
distance - distanceToNextPoint,
index + 1);
}
}
else {
// There are no further points. The distance exceeds the length
// of the full path. Return null.
return null;
}
}
var map = new GMap2(document.getElementById('map_canvas'));
var points = [
new GLatLng(47.656, -122.360),
new GLatLng(47.656, -122.343),
new GLatLng(47.690, -122.310),
new GLatLng(47.690, -122.270)
];
var polyline = new GPolyline(points, '#f00', 6);
var nextMarkerAt = 0; // Counter for the marker checkpoints.
var nextPoint = null; // The point where to place the next marker.
map.setCenter(new GLatLng(47.676, -122.343), 12);
// Draw the path on the map.
map.addOverlay(polyline);
// Draw the checkpoint markers every 1000 meters.
while (true) {
// Call moveAlongPath which will return the GLatLng with the next
// marker on the path.
nextPoint = moveAlongPath(points, nextMarkerAt);
if (nextPoint) {
// Draw the marker on the map.
map.addOverlay(new GMarker(nextPoint));
// Add +1000 meters for the next checkpoint.
nextMarkerAt += 1000;
}
else {
// moveAlongPath returned null, so there are no more check points.
break;
}
}
</script>
</body>
</html>
Screenshot of the above example, showing a marker every 1,000 meters:
These are the prototypes for the required functions:
google.maps.Polygon.prototype.Distance = function() {
var dist = 0;
for (var i=1; i < this.getPath().getLength(); i++) {
dist += this.getPath().getAt(i).distanceFrom(this.getPath().getAt(i-1));
}
return dist;
}
google.maps.LatLng.prototype.distanceFrom = function(newLatLng) {
//var R = 6371; // km (change this constant to get miles)
var R = 6378100; // meters
var lat1 = this.lat();
var lon1 = this.lng();
var lat2 = newLatLng.lat();
var lon2 = newLatLng.lng();
var dLat = (lat2-lat1) * Math.PI / 180;
var dLon = (lon2-lon1) * Math.PI / 180;
var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180 ) * Math.cos(lat2 * Math.PI / 180 ) *
Math.sin(dLon/2) * Math.sin(dLon/2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
var d = R * c;
return d;
}
source
Possibly the best approach would be to calculate where these points are.
As a basic algorithm you could iterate over all the points in the Polyline, and calculate the cumulative distance - if the next segment puts you over your distance, you can interpolate the point where the distance has been reached - then simply add a point of interest to your map for that.
I have used Martin Zeitler method to work with Google Map V3 and its working fine.
function init() {
var mapOptions = {
zoom: 15,
center: new google.maps.LatLng(-6.208437004433984, 106.84543132781982),
suppressInfoWindows: true,
};
// Get all html elements for map
var mapElement = document.getElementById('map1');
// Create the Google Map using elements
map = new google.maps.Map(mapElement, mapOptions);
var nextMarkerAt = 0; // Counter for the marker checkpoints.
var nextPoint = null; // The point where to place the next marker.
while (true) {
var routePoints = [ new google.maps.LatLng(47.656, -122.360),
new google.maps.LatLng(47.656, -122.343),
new google.maps.LatLng(47.690, -122.310),
new google.maps.LatLng(47.690, -122.270)];
nextPoint = moveAlongPath(routePoints, nextMarkerAt);
if (nextPoint) {
//Adding marker from localhost
MarkerIcon = "http://192.168.1.1/star.png";
var marker = new google.maps.Marker
({position: nextPoint,
map: map,
icon: MarkerIcon
});
// Add +1000 meters for the next checkpoint.
nextMarkerAt +=1000;
}
else {
// moveAlongPath returned null, so there are no more check points.
break;
}
}
}
Number.prototype.toRad = function () {
return this * Math.PI / 180;
}
Number.prototype.toDeg = function () {
return this * 180 / Math.PI;
}
function moveAlongPath(point, distance, index) {
index = index || 0; // Set index to 0 by default.
var routePoints = [];
for (var i = 0; i < point.length; i++) {
routePoints.push(point[i]);
}
if (index < routePoints.length) {
// There is still at least one point further from this point.
// Construct a GPolyline to use the getLength() method.
var polyline = new google.maps.Polyline({
path: [routePoints[index], routePoints[index + 1]],
strokeColor: '#FF0000',
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: '#FF0000',
fillOpacity: 0.35
});
// Get the distance from this point to the next point in the polyline.
var distanceToNextPoint = polyline.Distance();
if (distance <= distanceToNextPoint) {
// distanceToNextPoint is within this point and the next.
// Return the destination point with moveTowards().
return moveTowards(routePoints, distance,index);
}
else {
// The destination is further from the next point. Subtract
// distanceToNextPoint from distance and continue recursively.
return moveAlongPath(routePoints,
distance - distanceToNextPoint,
index + 1);
}
}
else {
// There are no further points. The distance exceeds the length
// of the full path. Return null.
return null;
}
}
function moveTowards(point, distance,index) {
var lat1 = point[index].lat.toRad();
var lon1 = point[index].lng.toRad();
var lat2 = point[index+1].lat.toRad();
var lon2 = point[index+1].lng.toRad();
var dLon = (point[index + 1].lng - point[index].lng).toRad();
// Find the bearing from this point to the next.
var brng = Math.atan2(Math.sin(dLon) * Math.cos(lat2),
Math.cos(lat1) * Math.sin(lat2) -
Math.sin(lat1) * Math.cos(lat2) *
Math.cos(dLon));
var angDist = distance / 6371000; // Earth's radius.
// Calculate the destination point, given the source and bearing.
lat2 = Math.asin(Math.sin(lat1) * Math.cos(angDist) +
Math.cos(lat1) * Math.sin(angDist) *
Math.cos(brng));
lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(angDist) *
Math.cos(lat1),
Math.cos(angDist) - Math.sin(lat1) *
Math.sin(lat2));
if (isNaN(lat2) || isNaN(lon2)) return null;
return new google.maps.LatLng(lat2.toDeg(), lon2.toDeg());
}
google.maps.Polyline.prototype.Distance = function () {
var dist = 0;
for (var i = 1; i < this.getPath().getLength(); i++) {
dist += this.getPath().getAt(i).distanceFrom(this.getPath().getAt(i - 1));
}
return dist;
}
google.maps.LatLng.prototype.distanceFrom = function (newLatLng) {
//var R = 6371; // km (change this constant to get miles)
var R = 6378100; // meters
var lat1 = this.lat();
var lon1 = this.lng();
var lat2 = newLatLng.lat();
var lon2 = newLatLng.lng();
var dLat = (lat2 - lat1) * Math.PI / 180;
var dLon = (lon2 - lon1) * Math.PI / 180;
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var d = R * c;
return d;
}
I wanted to port Daniel Vassalo's answer to iOS, but it wasn't worked properly and some markers were misplaced until I changed
var dLon = (point.lng() - this.lng()).toRad();
to
var dLon = point.lng().toRad() - this.lng().toRad();
So if anyone having a trouble to figure out why are the markers are misplaced, try this and maybe it will help.