Jan 5

How far from my way?

Posted by palako

Wife, children, pet, luggage, GPS, water, air pressure, oil levels, ……. everything is packed and ready for your deserved holidays, and there you go for your trip to Paris. Of course, you wan’t to be there as soon as possible, but you will have to stop to eat, at least two times a day, and you have this bunch of places you’ve been told to be worth stopping because of the great food they serve. Oh yes, they’re good, but you won’t go 30 miles away from your route just to eat. If only you could know how far from your straight way those places are …

20km       50km

The problem

Draw a polygon that surrounds a (poly)line in such a way than the outer line that forms the polygon is a given distance from the former line.

First things first

I will use the same structure for the code and examples we’ve been using in older posts, so you’ll be quite confortable if you’ve been following us (if not, you should, they are worth reading).

A brief abstract for new readers. Every piece of code here is javascript, we use the GoogleMaps API and very basic stuff from prototype (you don’t need to know anything about prototype to understand this) we have a GMap2 object, an array of points (of interest), an extension on the GPolygon object to determine if a Gpoint is contained in a polygon, and a piece of code we run on body load to put everything together and to add the listeners we are using.

Drawing the line

This is not what the post is about, so very quickly, take a look at the code and you’ll understand it:

function addListeners(map){

//... other listeners...
GEvent.addListener(map, "click", onClick);
}
var points = $A(); //Points array to draw the polyline
var lineOverlay =null;   //The polyline

function onClick(overlay,point) {

 points.push(point);
 if(lineOverlay!=null)
 	map.removeOverlay(lineOverlay);
 if(points.length>1){
 	lineOverlay = new GPolyline(points,"#0000ff",2);
 	map.addOverlay(lineOverlay);
 }
}

Here comes the math

To draw a GPolygon I’m first gathering all the points in an array and then passing the array to the GPolygon constructor.

For each point of the former polyline, two points for the polygon will arise. Imagine that our line is quite vertical. Then some points will be situated on its right and some on its left. But this pair of points for each point from the polyline are not contiguous in the polygon, but simetrically situated on the array. Let’s say I have a line made up of four points, A, B, C and D, in that order. I will have two points from every of those, let name them A1, A2, B1, B2, C1, C2, D1 and D2. If I start drawing the polygon from the point A, this will be the layout of the polygon points in the array: [A1, B1, C1, D1, D2, C2, B2,A2].

Ok, now, how to figure out the polygon points? A little math will do the work. Let’s take the first two points, A, and B, to go for points A1 and A2.

I want to figure out the position of two points at each side of the first point situated over the rect that goes through this point (A) and which incline is perpendicular to the segment that joins the point with the next point in the polyline (B).

I know points A and B coordinates, (Ax, Ay) and (Bx, By), therefore I can find the incline of the rect that goes through them both with the formula:

m1= Ay - By / Ax - Bx

and the incline of the perpendicular rect:

m2= -1 / m1

Coordinates of two points separated a given distance (d) over the rect that goes through point A with this last incline is a matter of trigonometry:

α = atan(m2)

A1x = Ax - d*cos(α)            A1y = Ay - d*sin(α)

A2x = d*cos(α) + Ax            A2Y = d*sin(α) + Ay

As this A point is the first in the iteration, we will add A1 and A2 to the polygon points arrat straight away. Remember, A1 as the first element and A2 as the last one.

paralels

And the rects that go through this points paralels to the AB segment are:

r1: y = m1 (x-A1x) + A1y

r2: y = m1 (x-A2x) + A2y

Time to iterate. Once you are here, repeat the operations for segment BC, that is, calculate BC incline, its perpendicular, look for two points over that perpendicular, B1 and B2, situated a distance “d” away from B, and finally calculate the ecuations of the paralel rects to BC that go through those two points.

r3: y = m3 (x-B’1x) + B’1y

r4: y = m3 (x-B’2x) + B’2y

Be careful here! Those B’1 and B’2 points are not to be added to the gpolygon. The points we are looking for are what we get intersecting those rects with the ones from the previous iteration. This is about solving a system with two ecuations and two variables, isolating one of the variables and them using its value on one ecuation to calculate the other.

Intersection

B1x = (-m3*B’1x+B’1y+m1*A1x-A1y)/(m1-m3)         B1y=m3*(B1x-B’1x)+B’1y

B2x=(-m3*B’2x+B’2y+m1*A2x-A2y)/(m1-m3)          B2y=m3*(B2x-B’2x)+B’2y

There you go, two more points to add. Remember that each of them go to one end on the array (for the B point, B1 would go to array[1] and B2 to array[array.length-2] (Do I need to say that javascript arrays are zero based? No I don’t…).

The hard work is already done, as this iterations will take you to the last point. For this last ones (D in our four points line example), you only need two points over the perpendicular, just as you did for the fisrt two. You already have the calculations for the incline of the perpendicular from last iteration, so you can straightly get the last pair of points (D1 and D2), which you will add to the array in the middle positions (this two will be contiguous).

If you get here and you understood everything, you won’t have any problem with the code, which is commented and is quite similar to what you’ve already read.

There’s always something tricky

When I first coded this, I spent several hours trying to figure out why I wasn’t working properly. The problem was that, sometimes, the lines of the polygon crossed their way of the line, and the “sometimes” happened to be when in a group of three points, they point in the middle was above of below the other two. Quite weird. Making some changes, What I obtained was that when in a group of three, if the point in the middle was the most left or right one, the lines crossed.

So, what did I do? Easy and miserable. I blame google for it and added a boolean “swapped” variable that I use to return the points where they should be before adding them to the array. You will clearly find and understand that also in the code.

What did I left behind

As the code is intended to be educational, there are some checks I didn’t do to keep it clear. I.e. keep in mind that horizontal segments will result in a division by zero when calculating the incline. Once you get that, mind it again to calculate the perpendicular. Same precautions must be taken when solving the ecuations system, as two consecutive segments with the same incline will result also in a division by zero error.

This aproach solves quite well the problem, but there are circunstances where you will find not the expected behaviours. That will happen, i.e. when you have segments in your line that are shorter than the distance you want the polygon to be drawn away.

Test it, download it, and leave me your comments.

Dec 25

Some days ago, Palako wrote an interesting post about drawing polygons with your mouse so we’re able to change the size of our “typical” circle. I’ve developed another way to do this: the circle has an icon which can modify its radius using the drag’n'drop feature of the GMarker.

Example Radius

When the circle is drawn, we put a marker on the circumference setting listeners for drag and drop events. But, we have the first problem because we have the central point of the circle, defined by its longitude and latitude, and the radius in kilometers. So, we need to know a point of the circumference in longitude and latitude:

var p1 = new GLatLng(actualPoint.lat()+0.1, actualPoint.lng());
var p2 = new GLatLng(actualPoint.lat(), actualPoint.lng()+0.1);
var latConv = actualPoint.distanceFrom(p1)/100;
var lngConv = actualPoint.distanceFrom(p2)/100;
var pint = new GLatLng(actualPoint.lat() + (newRadius/latConv *
  Math.cos(0 * Math.PI/180)), actualPoint.lng() + 
  (newRadius/lngConv * Math.sin(0 * Math.PI/180)));

So, we put the marker in that point adding the dragstart (for drag events) and the dragend (for drop events) listeners and they set a flag if the user is dragging the marker.

var markerOptions = { draggable: true };

marker=new GMarker(pint,markerOptions);
GEvent.addListener(marker, "dragstart", function() {
	document.getElementById("dragstart").innerHTML = "Drag start Coords: "
   + marker.getPoint().lat() + ", "+marker.getPoint().lng();
	drag=true;
});
GEvent.addListener(marker, "dragend", function() {
	checkDraggedMark();
	drag=false;
});
map.addOverlay(marker);

If the dragend listener is called or this flag is active and moving the marker (dragging the marker) the circle and contained PoIs are updated:

if (drag==true) {
  var newRadius=(actualPoint.distanceFrom(marker.getPoint())/1000);
  document.getElementById("dragend").innerHTML = "Radius: "+newRadius+" km";
  if (overlay)
   map.removeOverlay(overlay);
  overlay = drawCircle(actualPoint, newRadius, 30);
  showSelectedPois();
}

Of course, you can try this example or download the code!

Dec 6

Maps and Picasa

Posted by arcturus

The Google pic service, Picasa, offers us the possibilty to add geoinformation in a easy way.But it’s not so easy to retrieve the pics from a specific location.Endeed we are going to use an undocumented feature from Picasa API.

We asked Google guys to add a new feature, pics search by geoinformation, for GData Picasa API, and the redirected us to a post where explain how to do this:You have to add an extra param to the query pics inside the desired boundingbox:

http://picasaweb.google.com/data/feed/base/all?bbox= west,south,east,north

for example:

http://picasaweb.google.com/data/feed/base/all?bbox=-5.998,37.367,-5.953,37.403

But this query give us all the pics in this area, still we can’t use the bbox param in pic user searchs. What we can do if wanna get only our pics? We have a weird trick, in the name of the pics we have added a mark, and we use gdata query param to search for this mark, so our request looks like:

http://picasaweb.google.com/data/feed/base/all?bbox=west,south,east,north&q=mark_string

for example:

http://picasaweb.google.com/data/feed/base/all?bbox=-5.998,37.367,-5.953,37.403&q=nosvamosdetapas

Add two more params to complete the petition:

  1. max-results=100 : Limit the number of returned pics to 100.
  2. alt=json: Get the response in json format.

We encapsulate the call in a php file, and get the results using curl like this:

$curl_handle=curl_init();
$url ="http://picasaweb.google.com/data/feed/base/all?bbox=".$_GET["bounds"];
$url.="&q=nosvamosdetapas.com&maxresults=100&alt=json";
curl_setopt($curl_handle,CURLOPT_URL,$url);
curl_setopt($curl_handle,CURLOPT_CONNECTTIMEOUT,2);
curl_setopt($curl_handle,CURLOPT_RETURNTRANSFER,1);
$buffer = curl_exec($curl_handle);curl_close($curl_handle);
print $buffer;

The reason why we use a php script to retrieve the info is cause we can´t make a xhttprequest to an external server, so we make this xhttprequest to a script in the local domain.

We get a list of pics, in this pics we get the geodata information encoded, but most of the pics should have been taken from the same point, so our first task is extract geodata information and see how many different points we have pics for.We do this with tree javascript classes, ussing prototype 1.6:

var PicasaPic = Class.create({
initialize: function(jsonObj)
{
this.author = jsonObj.author.name;
this.title = jsonObj.title.$t;
this.thumb = jsonObj.media$group.media$thumbnail[0].url;
this.url = jsonObj.link[1].href
}
,
getAuthor: function(){ return this.author; }
,
getTitle: function() { return this.title; }
,
getThumb: function() { return this.thumb; }
,
getURL: function() { return this.url; }
});

This class will store all the data we need to know from a picture.

And this one, will represent a point of interest, poi, with an array of pics associated:

var PicPoi = Class.create({
initialize: function(point)
{
this.gpoint = point;
this.pics = $A();
}
,
getPoint: function(){ return this.gpoint; }
,
addPic: function(pic){ this.pics.push(pic); }
,
getPics: function(){ return this.pics; }
});

Our last class, the poi manager, a class that will check all the json entries and add pics to existing points or will create the points:

var PicPoiManager = Class.create({	initialize: function(){

 	this.picpois = $A();

 }

 ,

 analizePic: function(jsonObj, polygon){

 	var posArray = jsonObj.georss$where.gml$Point.gml$pos.$t.split(" ");

 	var point = new GLatLng(posArray[0],posArray[1]);

//Check again against the polygon bounding box

 	if(!polygon.contains(point))

 		return;

var found = false;

 	for(j=0;j
if (point.equals(this.picpois[j].getPoint())){

 			found = true;

 			this.picpois[j].addPic(new PicasaPic(jsonObj));

 		}

 	}if(!found){

 		var picpoi = new PicPoi(point);

 		picpoi.addPic(new PicasaPic(jsonObj));

 		this.picpois.push(picpoi);

 	}

}

 ,

 getPois: function(){	return this.picpois; }

 ,

 clear: function() { this.picpois = $A(); }

});

You can test the results here or download the source code from this link.

Nov 27

A slight modification to The bounding box problem will let us draw polygons (i.e. circles) centered anywhere on the map with arbitrary radius based on mouse actions. As the circle is drawn, points of interest within its area will be shown.

Drawing polygons caption

The dragging functionality is reserved to move the map, so we will start the drawing with a click and will finish it with another click. To let the user know that a drawing is being taken place, we will change the cursor.

For the sake of WYSIWYG, the polygon will be shown in real time as you move your mouse. A tiny line will show the radius conecting the center of the polygon with your mouse pointer.

So, first things first, capture the onclick event:

     GEvent.addListener(map, "click", onClick);

When the user clicks the map a new polygon drawing starts or ends. We are storing in a global variable the center of the polygon, so it can also be used to set the state. A null center means the drawing has not started, whereas a not null means you are actually drawing. That means:

If the drawing is starting (center was null):

  1. Clear any previous polygon or marker.
  2. Set the center of the polygon.
  3. Change the cursor to indicate the drawing state.

If the drawing is ending (center was set):

  1. Unset the center.
  2. Remove the Tiny line conecting the center with the edge.
function onClick(overlay,point) {
	if(myCenter==null) {
		myCenter=point;
		document.getElementById("map").firstChild.firstChild.style.cursor ="crosshair";
		map.clearOverlays();
	}
	else {
		myCenter=null;
		map.removeOverlay(lineOverlay);
	}
}

After a start drawing click, we want to see the polygon being drawn as we move the mouse, so we have to modify our mousemove callback:

  1. If a polygon (and its radius line) has been drawn before, remove it.
  2. Draw the new polygon and its radius tiny line.
  3. Draw points within the area of the polygon.
 	if(myCenter!=null) {
		if(overlay!=null)
			map.removeOverlay(overlay);
		if(lineOverlay!=null)
			map.removeOverlay(lineOverlay);
		var myRadius = myCenter.distanceFrom(point);
		overlay = drawCircle(myCenter, myRadius/1000,30);
		lineOverlay = new GPolyline(new Array(myCenter,point),"#0000ff",1);
		map.addOverlay(lineOverlay);
		showSelectedPois();
	}

One extra line in showSelectedPois() to hide the markers when they are not contained within the area of the polygon, and we are done.

		if (overlay.contains(item))
    			map.addOverlay(pois[i]);
    		else
   			pois[i].hide();

Feel free to test it or download it.

In future posts, we might resize the polygon once created and move the map as you aproach the edges.

Nov 25

I don’t like when a map is filled with markers covering all streets there. As there is no way to use vectorial icons for markers, I though I could write a sample which tries to emulate them.

So, we have the same icon in different sizes:

  • 8: 8 pixels of width
  • 12: 12 pixels of width
  • 16: 16 pixels of width

Now, we only need to choose which icon we want depending on the zoom level. So, we’re going to add a callback function for zoom events:

GEvent.addListener(map, "zoomend", zoomCallback);

function zoomCallback() {
	document.getElementById("zoom").innerHTML = "Zoom: " + map.getZoom();
	initPoIs();
	showSelectedPois();
}

The initPoIs() method checks if some markers have been created previously, and then it updates them using the appropiate icon:

function initPoIs() {
  var blueIcon = new GIcon(G_DEFAULT_ICON);
  if (map.getZoom()>=16) {
    blueIcon.image = "http://.../mark16.png";
    blueIcon.iconSize = new GSize(16, 16);
    blueIcon.shadowSize = new GSize(30, 16);
    blueIcon.iconAnchor = new GPoint(3, 16);
    blueIcon.infoWindowAnchor = new GPoint(7, 2);
    blueIcon.infoShadowAnchor = new GPoint(14, 14);
  }
  else if (map.getZoom()>=14&&map.getZoom()<=15) {
    blueIcon.image = "http://.../mark12.png";
    blueIcon.iconSize = new GSize(12, 12);
    blueIcon.shadowSize = new GSize(20, 12);
    blueIcon.iconAnchor = new GPoint(5, 12);
    blueIcon.infoWindowAnchor = new GPoint(5, 2);
    blueIcon.infoShadowAnchor = new GPoint(10, 10);
  }
  else {
    blueIcon.image = "http://.../mark8.png";
    blueIcon.iconSize = new GSize(8, 8);
    blueIcon.shadowSize = new GSize(14, 8);
    blueIcon.iconAnchor = new GPoint(3, 8);
    blueIcon.infoWindowAnchor = new GPoint(3, 2);
    blueIcon.infoShadowAnchor = new GPoint(6, 6);
  }

  markerOptions = { icon:blueIcon };

  for (var i=0; i < pois.length; i++) {
    map.removeOverlay(pois[i]);
  }
  for (var i=0; i < poisinfo.length; i++) {
    pois[i]=new GMarker(new GLatLng(poisinfo[i][0],poisinfo[i][1]), markerOptions);
  }

}

And, the showSelectedPois() method adds markers to the pois array in the map:

function showSelectedPois() {
  for (var i=0; i < pois.length && overlay; i++) {
    var item=pois[i].getLatLng();
    if (overlay.getBounds().containsLatLng(item)) {
      map.addOverlay(pois[i]);
    }
  }
}

You can try this example or download the code!

Nov 19

The boundingbox problem

Posted by arcturus

Again with google maps, now we are going to talk about the bounding box problem.

The google api provides the function containsBounds:

containsPoint(point)
Returns true if the rectangular area (inclusively) contains the pixel coordinates. (Since 2.88)

The problem is very simple, we wanna check points of interest inside the polygon, not the rectangular area that includes it.

boundingbox

We have solved this problem learning how to extend the GPolygon class with a method Contains that does what we want. You can see more examples of extending this class from this link.

Here is the code we used:

GPolygon.prototype.contains = function(point) { var j=0;

var oddNodes = false;

var x = point.lng();

var y = point.lat();

for (var i=0; i < this.getVertexCount(); i++) {

   j++;

   if (j == this.getVertexCount()) {j = 0;}   if (((this.getVertex(i).lat() < y)
         && (this.getVertex(j).lat() >= y))

         || ((this.getVertex(j).lat() < y) && (this.getVertex(i).lat() >= y))) {

              if ( this.getVertex(i).lng() + (y - this.getVertex(i).lat())

                   / (this.getVertex(j).lat()-this.getVertex(i).lat())

                  * (this.getVertex(j).lng() - this.getVertex(i).lng())>x){>

                       oddNodes = !oddNodes;

              }

    }

}

return oddNodes;

}

Of course you can test this code here and download it here.

Nov 15

First test with googlemaps

Posted by arcturus

Hi all,

this is the first example from a serie of tests written to probe the googlemaps capabilities.

This example is quite simple, to draw a circunference in the map, with a center point and a radius:

mapa0.png

Well, first of all, the google maps api does not draw circles, so, how is it that we see that circle on the map?

We are using here the solution provided by Esa from Helsinki. He has written a function that draws a polygon simulating a circunference, you can specify the number of polygon edges, better accuracy with more edges. Here is code:

 

function drawCircle(center, radius, nodes, liColor,
                           liWidth, liOpa, fillColor, fillOpa){

//http://esa.ilmari.googlepages.com/circle.htm

//calculating km/degree

var latConv = center.distanceFrom(new GLatLng(center.lat()
                                               +0.1, center.lng()))/100;

var lngConv = center.distanceFrom(new GLatLng(center.lat(), center.lng()
                                               +0.1))/100;

//Loop

var points = [];

var step = parseInt(360/nodes)||10;

for(var i=0; i<=360; i+=step) {

  var pint = new GLatLng(center.lat() + (radius/latConv *
                   Math.cos(i * Math.PI/180)), center.lng() +
                   (radius/lngConv * Math.sin(i * Math.PI/180)));

  points.push(pint);

  //bounds.extend(pint); //this is for fit function

}

fillColor = fillColor||liColor||"#2b82bd";

liWidth = liWidth||2;

var poly = new GPolygon(points,liColor,liWidth,liOpa,fillColor,fillOpa);

map.addOverlay(poly);

return poly;

}

Now adding some more functionality to the example from Esa, let´s draw a animation of an circunference growing until it reaches the whole area.

function drawAnimation(distance){

 timerStep = 0;

 timerId = setTimeout("drawAnimationAux("+distance+")",100);

}

function drawAnimationAux(distance){

 if (overlay)

   map.removeOverlay(overlay);

 var newRadius = distance/10.0 * (timerStep +1);

 overlay = drawCircle(actualPoint, newRadius, 30);

 timerStep++;

 if(timerStep<10)

    timerId = setTimeout("drawAnimationAux("+distance+")",100);

}

You can see this example working here, or download it.