tag:blogger.com,1999:blog-87437351450979540462024-03-04T12:31:46.132-05:00One Sad JamSoftware and Technology Notes from Adam JonesAdam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.comBlogger58125tag:blogger.com,1999:blog-8743735145097954046.post-56332727758634072642011-07-20T10:16:00.002-04:002011-07-20T10:21:21.934-04:00YAGRACYAGRAC was a fun project that I started both because I wanted a better Goodreads experience on my Android device and because I wanted to learn the Android SDK. I learned quite a bit from that experience, and I am very happy with the result. A while back, <a href="https://market.android.com/details?id=com.goodreads">Goodreads released their own official Android app</a>. The features of the official app largely mirror those of YAGRAC. Since there is now an official app, and because I haven't had the time to continue development like I would prefer, I'm probably not going to do any more releases of YAGRAC. I'll leave it on the market and keep the source available. I'm thrilled if you used YAGRAC and found it useful. It was a great project for me to learn on.Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0tag:blogger.com,1999:blog-8743735145097954046.post-55615378990305382002011-04-06T21:52:00.003-04:002011-04-06T21:59:46.427-04:00Countdown Clock Live Wallpaper<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiyXuoUSmtPKddSd8VfhTILPWnswX801sAWPXkiRJY3d61Zn9LTlN5RYb9u1Tm8EjjsAnBJU3J2qnDMkmPRJQxSelmjAXDDE0f06G2eNmZ3Slov48vf016Xs0Fuoef7-FSE6r0Xd4N2EGc/s1600/countdown_promo.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 180px; height: 120px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiyXuoUSmtPKddSd8VfhTILPWnswX801sAWPXkiRJY3d61Zn9LTlN5RYb9u1Tm8EjjsAnBJU3J2qnDMkmPRJQxSelmjAXDDE0f06G2eNmZ3Slov48vf016Xs0Fuoef7-FSE6r0Xd4N2EGc/s400/countdown_promo.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5592654480495383522" /></a>
As much as I hoped that my <a href="http://www.onesadjam.com/2011/03/last-call-for-google-io.html">Round 2 entry</a> for the <a href="https://sites.google.com/site/lastcallforio2011/Home">Last Call for Google I/O 2011</a> would win me a ticket to the conference, it wasn't in the cards. Still, I'm making lemonade from lemons. I've modified my entry to display as a live wallpaper, and I've added a bit of configuration so that a user can set whatever target date they would like to count down to. The <a href="https://market.android.com/details?id=com.onesadjam.CountdownLiveWallpaper">Countdown Clock Live Wallpaper</a> is available on the Android Market.Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0tag:blogger.com,1999:blog-8743735145097954046.post-38665860067412331172011-04-01T11:48:00.004-04:002011-04-01T11:51:52.970-04:00White - Now Available for Android<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPsNBBekEC04j_JUgAqhQu39_4jRNXdO78JK60_bcQHq0TqjwIGfacB6zKmMR1NoCh0xW9lDjwL0KgElfu6K7-0vGMsQdXuS8Vx29CbK212-YebfQQGbocwYPu0LEhUK8zqIxYXvlZH84/s1600/WhitePromo.png"><img style="cursor:pointer; cursor:hand;width: 180px; height: 120px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPsNBBekEC04j_JUgAqhQu39_4jRNXdO78JK60_bcQHq0TqjwIGfacB6zKmMR1NoCh0xW9lDjwL0KgElfu6K7-0vGMsQdXuS8Vx29CbK212-YebfQQGbocwYPu0LEhUK8zqIxYXvlZH84/s400/WhitePromo.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5590642823977494882" /></a>
<p>I was listening to <a href="http://twit.tv/294">This Week in Tech</a> on commute and heard <a href="http://www.baratunde.com/">Baratunde Thurston</a> comment that he would like to see an app called White in response to <a href="http://color.com/">COLOR</a>. I was inspired, and today released White on the Android Market :-)</p>
<p>Download today! <a href="https://market.android.com/details?id=com.onesadjam.white">https://market.android.com/details?id=com.onesadjam.white</a></p>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0tag:blogger.com,1999:blog-8743735145097954046.post-39742298731427539072011-03-17T12:23:00.004-04:002011-03-17T12:33:35.394-04:00Last Call for Google I/O<p>Google is running a <a href="https://sites.google.com/site/lastcallforio2011/">series of contests</a> this week to give away the last remaining tickets to the Google I/O developer conference. The 2011 conference sold out in record time, so for me, this is about the only way I'm going to have a chance to go. Yesterday was the start of the first challenge which focused on Android. Due to the time difference, the 30 minute "lightning" round 1 started during my normal lunch break, and I was able to quickly submit my answers. I was pleasantly surprised to receive an e-mail 30 minutes later informing me that I was one of 200 entrants to pass round 1 and move on to round 2.</p>
<p>For round 2, the objective was to create and submit an Android app that recreates the <a href="http://www.google.com/events/io/2011/">bouncing balls countdown clock seen on the Google I/O home page</a>. Of course, today being St. Patrick's day, the clock is slightly changed to be formed of clover leaves blowing in the wind, but on non-holidays it is multicolored balls. Contestants had 22 hours to complete their app and submit, with the deadline being 9am pacific time this morning.</p>
<p>Now that the deadline has passed, I thought I would share my entry. I'm publishing both the source code and the APK file, so if you are curious how I made it, feel free to take a look.</p>
<ul>
<li>APK to install on your Android device - <a href="http://dl.dropbox.com/u/2974623/LastCallForGoogleIO.apk">LastCallForGoogleIO.apk</a></li>
<li>ZIP file containing source files - <a href="http://dl.dropbox.com/u/2974623/LastCallForGoogleIO.zip">LastCallForGoogleIO.zip</a></li>
</ul>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com15tag:blogger.com,1999:blog-8743735145097954046.post-53489967500757808362010-08-31T17:22:00.003-04:002010-08-31T17:33:58.379-04:00YAGRAC<p>I've been wanting to learn how to develop for the Android platform, and I recently took some time to start a project to do just that. <a href="http://www.onesadjam.com/p/yagrac.html">YAGRAC is Yet Another GoodReads Android Client</a>. I've made the source code open source, so feel free to <a href="http://code.google.com/p/yagrac/">take a look</a> or <a href="http://code.google.com/p/yagrac/downloads/list">download the client</a> and let me know what you think.</p>
<p><a href="http://www.goodreads.com/">GoodReads</a> is a social book reading service that I am a huge fan of. You can keep track of books that you have read, want to read, or are currently reading. Your friends can follow your list of books to see what you are up to. It is great for finding like-minded readers and discovering new books to enjoy. The GoodReads site is great, but when I'm away from my desk the mobile site leaves me wishing for more. GoodReads does not have an official app for iPhone or Android, so I though, why not make one myself! So far I have implemented the ability to read updates from friends, browse books on my own shelves or someone else's shelves, search for books, and review my list of social contacts (friends, followers, and following).</p>
<p>This project has proven to be a very good and effective learning opportunity. As I encounter interesting bits I will be sure to share them here.</p>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0tag:blogger.com,1999:blog-8743735145097954046.post-16564588702100532562010-05-27T09:57:00.011-04:002010-05-27T16:40:20.620-04:00Creating a RESTful web service using WCF and JSON<div>There are a vast number of technologies to choose from for implementing a web application. You can choose Flash, Silverlight, Java, HTML, JavaScript, CSS, ASP.NET, PHP, and a host of others. Putting aside arguments for or against such an approach, let's examine how we might create a RESTful web service in WCF that accepts and emits JSON.</div>
<div>
First, we need to create a project. Create a new ASP.NET web application.
<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrHl6iAeLVNQGNCs92oraa0V5qy1MHj3oKafI5lyeRRd6Fa5sdLwpYlzMuImy26VPp5gNG-lo5QJUuAZ4kDP0aCthaj-AsISTuf8Z91v6zlZfWbD1eTWIudkADSqt5uuPSqbk4jAtGd1M/s1600/RESTfulWCF+-+New+Project.JPG"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 270px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrHl6iAeLVNQGNCs92oraa0V5qy1MHj3oKafI5lyeRRd6Fa5sdLwpYlzMuImy26VPp5gNG-lo5QJUuAZ4kDP0aCthaj-AsISTuf8Z91v6zlZfWbD1eTWIudkADSqt5uuPSqbk4jAtGd1M/s400/RESTfulWCF+-+New+Project.JPG" border="0" alt="" id="BLOGGER_PHOTO_ID_5475954308505787906" /></a>
We can delete the Default.aspx file as we will not be using it.</div>
<div>Next, let's add a WCF service to our project. When naming this service, keep in mind that this is your access point to the resource that is represented in our RESTful interface. If we are talking about Widgets, we might want to name it WidgetManager.svc.
<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjs4jAw3GJPpfZCdLFECgUx5l-jpp7aaObtE4P9NS7Sn3kVApXUrUem-wQ27dXDEsw55DOYLZw0jvAuwuDvLrKJPjNwKl6Co4fTKAI_v4NydJ1346g7pFhyphenhyphenxklsmT7vIdacbB_4UbwN0hs/s1600/RESTfulWCF+-+Add+WCF+Service.JPG"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 241px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjs4jAw3GJPpfZCdLFECgUx5l-jpp7aaObtE4P9NS7Sn3kVApXUrUem-wQ27dXDEsw55DOYLZw0jvAuwuDvLrKJPjNwKl6Co4fTKAI_v4NydJ1346g7pFhyphenhyphenxklsmT7vIdacbB_4UbwN0hs/s400/RESTfulWCF+-+Add+WCF+Service.JPG" border="0" alt="" id="BLOGGER_PHOTO_ID_5475955558221209618" /></a>
This will create three files for us:
<ul><li>IWidgetManager.cs - The interface describing service contract</li><li>WidgetManager.svc - The web service definition (markup)</li><li>WidgetManager.svc.cs - The web service implementation</li></ul><div>First examine the code for our contract.</div>
</div>
<div class="code-google"><code class="prettyprint">
using System.ServiceModel;
namespace RESTfulWCF
{
[ServiceContract]
public interface IWidgetManager
{
[OperationContract]
void DoWork();
}
}
</code></div>
<div>We need to indicate that this web service will be using JSON for communicating to the client. To do that we modify the service method description.</div>
<div class="code-google"><code class="prettyprint">
[OperationContract]
[WebInvoke(
ResponseFormat = WebMessageFormat.Json,
BodyStyle = WebMessageBodyStyle.Bare)]
void DoWork();
</code></div>
<div>Notice that we need to add a reference to System.ServiceModel.Web to our project and add a using statement to include the definition of the WebInvoke attribute. The next step is to indicate which of the REST actions (GET, PUT, POST, DELETE) the method will correspond to, and what REST URL format is used to call this method. Let's say that our web service method is intended to retrieve a list of all of the widgets known to the system.</div>
<div class="code-google"><code class="prettyprint">
[OperationContract]
[WebInvoke(
Method = "GET",
ResponseFormat = WebMessageFormat.Json,
BodyStyle = WebMessageBodyStyle.Bare,
UriTemplate = "Widgets")]
void GetWidgets();
</code></div>
<div>That completes the definition of our web service contract. Assuming our site is hosted at the root of localhost, it is accessed using a GET request against the URL http://localhost/WidgetManager.svc/Widgets and will respond with JSON. But wait, our response type is void. Let's define a complex data type to represent the properties of our widget and return a collection of those widgets from our method.</div>
<div class="code-google"><code class="prettyprint">
using System.Runtime.Serialization;
namespace RESTfulWCF
{
[DataContract]
public class Widget
{
[DataMember]
public string Name { get; set; }
[DataMember]
public int SprocketSize { get; set; }
[DataMember]
public int CogCount { get; set; }
}
}
</code></div>
<div>And we update our return type to be a collection of this type.</div>
<div class="code-google"><code class="prettyprint">
[OperationContract]
[WebInvoke(
Method = "GET",
ResponseFormat = WebMessageFormat.Json,
BodyStyle = WebMessageBodyStyle.Bare,
UriTemplate = "Widgets")]
Widget[] GetWidgets();
</code></div>
<div>We need the function to return at least one item in our array to effectively demonstrate this code. Open the WidgetManager.svc.cs code behind file and implement the web service.</div>
<div class="code-google"><code class="prettyprint">
using System.Collections.Generic;
namespace RESTfulWCF
{
public class WidgetManager : IWidgetManager
{
public Widget[] GetWidgets()
{
List<Widget> widgets = new List<Widget>();
widgets.Add(
new Widget
{
CogCount = 3,
Name = "Widget Alpha",
SprocketSize = 6
} );
return widgets.ToArray();
}
}
}
</code></div>
<div>We now need to tweak our web.config file so that the web server and the class description are in sync. We could define our service behavior in the web.config as well, but in this case it is easier to use a factory to set that up on our behalf. Open the WidgetManager.svc file markup and add the Factory.</div>
<div class="code-google"><code class="prettyprint">
<%@ ServiceHost Language="C#"
Debug="true"
Service="RESTfulWCF.WidgetManager"
CodeBehind="WidgetManager.svc.cs"
Factory="System.ServiceModel.Activation.WebServiceHostFactory"
%>
</code></div>
<div>We still need to make a change to our web.config, but now we only need to define the endpoint behavior.</div>
<div class="code-google"><code class="prettyprint">
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="RESTfulWCF.WidgetManagerEndpointBehavior">
<webHttp/>
</behavior>
</endpointBehaviors>
</behaviors>
<services>
<service name="RESTfulWCF.WidgetManager">
<endpoint
address=""
binding="webHttpBinding"
behaviorConfiguration="RESTfulWCF.WidgetManagerEndpointBehavior"
contract="RESTfulWCF.IWidgetManager">
</endpoint>
</service>
</services>
</system.serviceModel>
</code></div>
<div>That's all we need to get started with this simple example. Compile the project and run. You will see a webpage that indicates the endpoint was not found. This is because the default start page is the service file, rather than the REST URL. Modify the URL to http://localhost/WidgetManager.svc/Widgets and you will get the JSON result emitted back.</div><div>
</div>
<div class="code-google"><span class="Apple-style-span" style=" ;font-size:medium;"><span class="Apple-style-span" style="font-family:'courier new';">[{"CogCount":3,"Name":"Widget Alpha","SprocketSize":6}]</span></span></div>
<div>The next thing we will want to do is accept a parameter in the request. To do this, create a new service method that accepts a parameter, in our case the ID of the widget to return.</div>
<div class="code-google"><code class="prettyprint">
[OperationContract]
[WebInvoke(
Method = "GET",
ResponseFormat = WebMessageFormat.Json,
BodyStyle = WebMessageBodyStyle.Bare,
UriTemplate = "Widgets/{widgetId}")]
Widget GetWidget(string widgetId);
</code></div>
<div>And remember to implement the method in our code behind file.</div>
<div class="code-google"><code class="prettyprint">
public Widget GetWidget(string widgetId)
{
return
new Widget
{
CogCount = 3,
Name = widgetId,
SprocketSize = 6
};
}
</code></div>
<div>Now when we run the project and browse to the URL http://localhost/WebService.svc/Widgets/117 we see the output widget carries the name we supplied in the URL.
</div>
<div>
<span class="Apple-style-span" style="font-family:'Times New Roman';font-size:medium;">
{"CogCount":3,"Name":"117","SprocketSize":6}
</span></div>
<div>An important note here is that the parameters passed in to the web service method in this way must all be strings. A UriTemplate may contain as many parameters as you like, and may appear anywhere in the URI. So a URI template of 'Widgets/{widgetId}/Cogs/{cogId}/Color' is completely acceptable.
</div>
<div>This is just scratching the surface though, as we still haven't touched on the other three REST operations (PUT, POST, and DELETE). There are a number of options on the data contract attributes that allow you to modify the names of the JSON elements, as well as the order they appear in the JSON string. Source code for the example in this post is available here:</div>
<div><a href="http://dl.dropbox.com/u/2974623/onesadjam/RESTfulWCF.zip">http://dl.dropbox.com/u/2974623/onesadjam/RESTfulWCF.zip</a>
</div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com3tag:blogger.com,1999:blog-8743735145097954046.post-21320669047981541002010-05-21T22:21:00.005-04:002010-05-21T22:29:37.515-04:00Indy Tech Fest 2010 Slides<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://dl.dropbox.com/u/2974623/Getting%20Started%20with%20Online%20Mapping%20Services.pdf"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 301px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEicyoBJ_b4uuT19OVLUpjN8pCsh6B6jmYsnt_ijJ1TiZHamVmhsW1GyM2HknIfp3p1OvbC2Gzmh3lgIichayWy6zvB9_kofnib-1wWWg4bOAbnKA1k0cphI_grTYVnWvpMOpfUBbm2kvxU/s400/ITF.png" border="0" alt="" id="BLOGGER_PHOTO_ID_5473914353497558930" /></a>Click the image above to download my slide deck for "Getting Started with Online Mapping Services" presented at Indy Tech Fest 2010.<div>
</div><div><a href="http://dl.dropbox.com/u/2974623/Getting%20Started%20with%20Online%20Mapping%20Services.zip">Sample code available as well.</a></div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0tag:blogger.com,1999:blog-8743735145097954046.post-89696581756907299992010-04-19T01:05:00.003-04:002010-04-19T01:14:05.029-04:00Developing with Online Mapping APIs - Part 6: Custom Drawing<div>
<p>At this point we've covered all of the basics. You have all the tools you need to display an interactive map with rich information. You also have some information that will help you to decide which vendor API is best suited for your task. Now let's do something a little more fun. Let's try doing some custom drawing on our maps. Custom drawing is the ability to draw lines and polygons (filled and unfilled) on our map, and potentially to even overlay text and images. The degree to which you can plot custom elements on the map varies across mapping APIs, so let's take a look at what kind of interesting things we can do on each.</p>
</div>
<h3>Bing</h3>
<div class="mapDiv" id="bingMapDiv6"></div>
<script type="text/javascript">
var bingMap6;
var bingMarker6;
function bingInitializer6()
{
bingMap6 = new VEMap('bingMapDiv6');
bingMap6.SetDashboardSize(VEDashboardSize.Normal);
var mapCenter = new VELatLong(39.768519, -86.158041);
bingMap6.LoadMap(mapCenter, 13);
bingMap6.ShowDashboard();
bingPlotLines();
bingPlotCircle(0.6);
bingPlotCircle(0.5);
bingPlotCircle(0.4);
bingPlotCircle(0.3);
bingPlotCircle(0.2);
bingPlotImage();
bingPlotText();
}
scriptInitializers.push(bingInitializer6);
function bingPlotCircle(scalar)
{
var vertexCount = 64;
var vertices = new Array();
var v = 0;
var radians = Math.PI / 180;
var radiansPerDegree = Math.PI / 180;
var degreesPerVertex = 360 / vertexCount;
var radiansPerVertex = degreesPerVertex * radiansPerDegree;
var mapCenterLatLong = bingMap6.GetCenter();
var mapView = bingMap6.GetMapView();
var mapHeight = mapView.TopLeftLatLong.Latitude - mapCenterLatLong.Latitude;
var mapWidth = mapView.TopLeftLatLong.Longitude - mapCenterLatLong.Longitude;
for ( v = 0; v < vertexCount; v++ )
{
radians = v * radiansPerVertex;
vertices.push(
new VELatLong(
mapCenterLatLong.Latitude + ( scalar * mapHeight * Math.sin( radians ) ),
mapCenterLatLong.Longitude + ( scalar * mapWidth * Math.cos( radians ) ) ) );
}
var circle = new VEShape(VEShapeType.Polygon, vertices);
circle.SetFillColor( new VEColor( 128, 0, 0, 0.2 ) );
circle.SetLineColor( new VEColor( 128, 0, 0, 0.2 ) );
circle.SetLineWidth( 1 );
circle.HideIcon();
bingMap6.AddShape( circle );
}
function bingPlotLines()
{
var vertices = new Array();
var bounds = bingMap6.GetMapView();
var center = bingMap6.GetCenter();
vertices.push( new VELatLong( bounds.TopLeftLatLong.Latitude, center.Longitude ) );
vertices.push( new VELatLong( bounds.BottomRightLatLong.Latitude, center.Longitude ) );
var verticalLine = new VEShape( VEShapeType.Polyline, vertices );
verticalLine.SetLineWidth(6);
verticalLine.HideIcon();
bingMap6.AddShape(verticalLine);
vertices = new Array();
vertices.push( new VELatLong( center.Latitude, bounds.TopLeftLatLong.Longitude ) );
vertices.push( new VELatLong( center.Latitude, bounds.BottomRightLatLong.Longitude ) );
var horizontalLine = new VEShape( VEShapeType.Polyline, vertices );
horizontalLine.SetLineWidth(6);
horizontalLine.HideIcon();
bingMap6.AddShape(horizontalLine);
}
function bingPlotText()
{
var textShape = new VEShape( VEShapeType.Pushpin, bingMap6.GetCenter() );
textShape.SetCustomIcon("<span>BULLSEYE</span>");
bingMap6.AddShape(textShape);
}
function bingPlotImage()
{
var imageShape = new VEShape( VEShapeType.Pushpin, bingMap6.GetCenter() );
imageShape.SetCustomIcon("https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvPD-ydg5-zBkX7kgIpop4lssNoO4RsvRM1tikkNWigSzvwOrDMhX0vRXrSvwipd6om4cALIetzl7kC3gQjKcsZ5fif8o6kosOJk_loogTVkQte51GZ6XwsLV_taFqdyoNWlnIlMsqxqI/s400/target.jpg");
bingMap6.AddShape(imageShape);
}
</script>
<div><p>The VEShape class provides all the tools we need for doing custom drawing on a Bing map. With a VEShape instance we can:</p>
<ul>
<li>Draw text
<li>Plot an image
<li>Draw a polyline (a line with multiple segments)
<li>Draw an unfilled polygon
<li>Draw a filled polygon
</ul>
<p>We can control the color of the text, line, and fill. We can also assign a transparency value (alpha) to the fill and line color. When we use a VEShape, the shape we put on the map is anchored to the map at the lattitude and longitude of the points we assign.</p>
<p>Adding text to the map is as simple as creating a VEShape of type VEShapeType.Pushpin and then assigning the text to display using the SetCustomIcon() method. Be sure to wrap the text in a SPAN or DIV tag, otherwise the text will be interpretted as the URL of an image. Also, the Bing map will wrap this custom HTML in an A tag, so you may want to assign some styles to your DIV or SPAN tag to be sure the text is displayed the way you want. As mentioned, as long as the argument given to the SetCustomIcon method doesn't start with a '<', it is assumed to be the URL of an image to display. This is how we can display an image on our map.</p>
<p>When creating a polygon using the Bing map API, an array of vertices is passed to the VEShape constructor. When drawing the shape, the API will automatically creating a line segment between the last vertex and the first vertex. The API only supports line segments (no arcs) so it is not possible to draw smooth curves. Still, we can fake up a curves by using a number of small line segments.</p>
<div class="code-bing"><pre>
function bingPlotCircle(scalar)
{
var vertexCount = 64;
var vertices = new Array();
var v = 0;
var radians = Math.PI / 180;
var radiansPerDegree = Math.PI / 180;
var degreesPerVertex = 360 / vertexCount;
var radiansPerVertex = degreesPerVertex * radiansPerDegree;
var mapCenterLatLong = bingMap6.GetCenter();
var mapView = bingMap6.GetMapView();
var mapHeight = mapView.TopLeftLatLong.Latitude - mapCenterLatLong.Latitude;
var mapWidth = mapView.TopLeftLatLong.Longitude - mapCenterLatLong.Longitude;
for ( v = 0; v < vertexCount; v++ )
{
radians = v * radiansPerVertex;
vertices.push(
new VELatLong(
mapCenterLatLong.Latitude + ( scalar * mapHeight * Math.sin( radians ) ),
mapCenterLatLong.Longitude + ( scalar * mapWidth * Math.cos( radians ) ) ) );
}
var circle = new VEShape(VEShapeType.Polygon, vertices);
circle.SetFillColor( new VEColor( 128, 0, 0, 0.2 ) );
circle.SetLineColor( new VEColor( 128, 0, 0, 0.2 ) );
circle.SetLineWidth( 1 );
circle.HideIcon();
bingMap6.AddShape( circle );
}
function bingPlotLines()
{
var vertices = new Array();
var bounds = bingMap6.GetMapView();
var center = bingMap6.GetCenter();
vertices.push( new VELatLong( bounds.TopLeftLatLong.Latitude, center.Longitude ) );
vertices.push( new VELatLong( bounds.BottomRightLatLong.Latitude, center.Longitude ) );
var verticalLine = new VEShape( VEShapeType.Polyline, vertices );
verticalLine.SetLineWidth(6);
verticalLine.HideIcon();
bingMap6.AddShape(verticalLine);
vertices = new Array();
vertices.push( new VELatLong( center.Latitude, bounds.TopLeftLatLong.Longitude ) );
vertices.push( new VELatLong( center.Latitude, bounds.BottomRightLatLong.Longitude ) );
var horizontalLine = new VEShape( VEShapeType.Polyline, vertices );
horizontalLine.SetLineWidth(6);
horizontalLine.HideIcon();
bingMap6.AddShape(horizontalLine);
}
function bingPlotText()
{
var textShape = new VEShape( VEShapeType.Pushpin, bingMap6.GetCenter() );
textShape.SetCustomIcon("<span>BULLSEYE</span>");
bingMap6.AddShape(textShape);
}
function bingPlotImage()
{
var imageShape = new VEShape( VEShapeType.Pushpin, bingMap6.GetCenter() );
imageShape.SetCustomIcon("target.jpg");
bingMap6.AddShape(imageShape);
}
</pre></div>
</div>
<h3>Google</h3>
<div class="mapDiv" id="googleMapDiv6"></div>
<script type="text/javascript">
var googleMap6;
function googleInitializer6()
{
googleMap6 = new GMap2(document.getElementById("googleMapDiv6"));
googleMap6.setCenter(new GLatLng(39.768519, -86.158041), 13);
googleCurrentMapControlStyle = new GLargeMapControl3D();
googleMap6.addControl(googleCurrentMapControlStyle);
googlePlotLines();
googlePlotCircle(0.6);
googlePlotCircle(0.5);
googlePlotCircle(0.4);
googlePlotCircle(0.3);
googlePlotCircle(0.2);
googlePlotImage();
}
scriptInitializers.push(googleInitializer6);
function googlePlotCircle(scalar)
{
var vertexCount = 64;
var vertices = new Array();
var v = 0;
var radians = Math.PI / 180;
var radiansPerDegree = Math.PI / 180;
var degreesPerVertex = 360 / vertexCount;
var radiansPerVertex = degreesPerVertex * radiansPerDegree;
var center = googleMap6.getCenter();
var bounds = googleMap6.getBounds();
var mapHeight = bounds.getNorthEast().lat() - center.lat();
var mapWidth = bounds.getNorthEast().lng() - center.lng();
for ( v = 0; v < vertexCount; v++ )
{
radians = v * radiansPerVertex;
vertices.push(
new GLatLng(
center.lat() + ( scalar * mapHeight * Math.sin( radians ) ),
center.lng() + ( scalar * mapWidth * Math.cos( radians ) ) ) );
}
var circle = new GPolygon(vertices, "#800000", 1, 0.2, "#800000", 0.2);
googleMap6.addOverlay( circle );
}
function googlePlotLines()
{
var vertices = new Array();
var bounds = googleMap6.getBounds();
var center = googleMap6.getCenter();
vertices.push( new GLatLng( bounds.getSouthWest().lat(), center.lng() ) );
vertices.push( new GLatLng( bounds.getNorthEast().lat(), center.lng() ) );
var verticalLine = new GPolyline( vertices, "#0000FF", 6 );
googleMap6.addOverlay(verticalLine);
vertices = new Array();
vertices.push( new GLatLng( center.lat(), bounds.getSouthWest().lng() ) );
vertices.push( new GLatLng( center.lat(), bounds.getNorthEast().lng() ) );
var horizontalLine = new GPolyline( vertices, "#0000FF", 6 );
googleMap6.addOverlay(horizontalLine);
}
function googlePlotImage()
{
var imageIcon = new GIcon( G_DEFAULT_ICON, "https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvPD-ydg5-zBkX7kgIpop4lssNoO4RsvRM1tikkNWigSzvwOrDMhX0vRXrSvwipd6om4cALIetzl7kC3gQjKcsZ5fif8o6kosOJk_loogTVkQte51GZ6XwsLV_taFqdyoNWlnIlMsqxqI/s400/target.jpg" );
var imageShape = new GMarker( googleMap6.getCenter(), { icon:imageIcon } );
googleMap6.addOverlay(imageShape);
}
</script>
<div>
<p>You know the story by now - where Bing consolidates functionality to a single class, Google separates into many classes. The same holds for custom drawing. With Google Maps we can:</p>
<ul>
<li>Plot an image
<li>Draw a polyline (a line with multiple segments)
<li>Draw an unfilled polygon
<li>Draw a filled polygon
</ul>
<p>Unlike Bing, which used the VEShape class for all of these, Google uses several:</p>
<ul>
<li>GIcon
<ul><li>Plot a custom image</ul>
<li>GPolyline
<ul><li>Draw a polyline</ul>
<li>GPolygon
<ul><li>Draw a polygon (filled or unfilled)</ul>
</ul>
<p>Another difference between the Bing and Google implementation is that with the Google classes, most of the options for the shape are set in the constructor. Using named parameters we can set the line width, color, opacity, fill color, and fill opacity.</p>
<div class="code-google"><pre>
function googlePlotCircle(scalar)
{
var vertexCount = 64;
var vertices = new Array();
var v = 0;
var radians = Math.PI / 180;
var radiansPerDegree = Math.PI / 180;
var degreesPerVertex = 360 / vertexCount;
var radiansPerVertex = degreesPerVertex * radiansPerDegree;
var center = googleMap6.getCenter();
var bounds = googleMap6.getBounds();
var mapHeight = bounds.getNorthEast().lat() - center.lat();
var mapWidth = bounds.getNorthEast().lng() - center.lng();
for ( v = 0; v < vertexCount; v++ )
{
radians = v * radiansPerVertex;
vertices.push(
new GLatLng(
center.lat() + ( scalar * mapHeight * Math.sin( radians ) ),
center.lng() + ( scalar * mapWidth * Math.cos( radians ) ) ) );
}
var circle = new GPolygon(vertices, "#800000", 1, 0.2, "#800000", 0.2);
googleMap6.addOverlay( circle );
}
function googlePlotLines()
{
var vertices = new Array();
var bounds = googleMap6.getBounds();
var center = googleMap6.getCenter();
vertices.push( new GLatLng( bounds.getSouthWest().lat(), center.lng() ) );
vertices.push( new GLatLng( bounds.getNorthEast().lat(), center.lng() ) );
var verticalLine = new GPolyline( vertices, "#0000FF", 6 );
googleMap6.addOverlay(verticalLine);
vertices = new Array();
vertices.push( new GLatLng( center.lat(), bounds.getSouthWest().lng() ) );
vertices.push( new GLatLng( center.lat(), bounds.getNorthEast().lng() ) );
var horizontalLine = new GPolyline( vertices, "#0000FF", 6 );
googleMap6.addOverlay(horizontalLine);
}
function googlePlotImage()
{
var imageIcon = new GIcon( G_DEFAULT_ICON, "target.jpg" );
var imageShape = new GMarker( googleMap6.getCenter(), { icon:imageIcon } );
googleMap6.addOverlay(imageShape);
}
</pre></div>
<p>One key difference here is that the Google Maps API does not supply a method to plot text onto the map. There are a few workaround options. One is to create an image containing the text, but this makes internationalization of the text difficult. Another workaround is to use a GIcon marker and display the desired text when the info window is displayed. This has the drawback of not displaying the text when the info window is hidden. A third option is to implement your own overlay class that inherits from GMarker, such as <a href="http://www.googlemapsbook.com/2007/01/22/extending-gmarker/">this LabeledMarker demonstrated by Michael Purvis</a>. Finally, there are third-party libraries such as <a href="http://sub1.econym.org.uk/gmap/elabel.htm">ELabel</a> that include text overlay functionality. Even with these options, not including a text overlay in the stock API seems to be an oversight in my opinion.</p>
</div>
<h3>Yahoo</h3>
<div class="mapDiv" id="yahooMapDiv6"></div>
<script type="text/javascript">
var yahooMap6;
function yahooInitializer6()
{
yahooMap6 = new YMap(document.getElementById('yahooMapDiv6'));
yahooMap6.addPanControl();
yahooMap6.addTypeControl();
yahooMap6.addZoomLong();
yahooMap6.addZoomScale();
yahooMap6.drawZoomAndCenter(new YGeoPoint(39.768519, -86.158041), 5);
yahooPlotLines();
yahooPlotCircle(0.6);
yahooPlotCircle(0.5);
yahooPlotCircle(0.4);
yahooPlotCircle(0.3);
yahooPlotCircle(0.2);
yahooPlotImage();
yahooPlotText();
}
scriptInitializers.push(yahooInitializer6);
function yahooPlotCircle(scalar)
{
var vertexCount = 64;
var vertices = new Array();
var v = 0;
var radians = Math.PI / 180;
var radiansPerDegree = Math.PI / 180;
var degreesPerVertex = 360 / vertexCount;
var radiansPerVertex = degreesPerVertex * radiansPerDegree;
var center = yahooMap6.getCenterLatLon();
var bounds = yahooMap6.getBoundsLatLon();
var mapHeight = bounds.LatMax - center.Lat;
var mapWidth = bounds.LonMax - center.Lon;
for ( v = 0; v < vertexCount; v++ )
{
radians = v * radiansPerVertex;
vertices.push(
new YGeoPoint(
center.Lat + ( scalar * mapHeight * Math.sin( radians ) ),
center.Lon + ( scalar * mapWidth * Math.cos( radians ) ) ) );
}
vertices.push( vertices[0] );
var circle = new YPolyline(vertices, "#FF0000", 3);
yahooMap6.addOverlay( circle );
}
function yahooPlotLines()
{
var vertices = new Array();
var bounds = yahooMap6.getBoundsLatLon();
var center = yahooMap6.getCenterLatLon();
vertices.push( new YGeoPoint( bounds.LatMax, center.Lon ) );
vertices.push( new YGeoPoint( bounds.LatMin, center.Lon ) );
var verticalLine = new YPolyline( vertices, "#0000FF", 6 );
yahooMap6.addOverlay(verticalLine);
vertices = new Array();
vertices.push( new YGeoPoint( center.Lat, bounds.LonMax ) );
vertices.push( new YGeoPoint( center.Lat, bounds.LonMin ) );
var horizontalLine = new YPolyline( vertices, "#0000FF", 6 );
yahooMap6.addOverlay(horizontalLine);
}
function yahooPlotText()
{
var labelNode = YUtility.createNode('div', 'yahooLabelOverlay');
labelNode.style.color = "#0000FF";
labelNode.style.backgroundColor = "#FFFFFF";
labelNode.innerHTML += 'BULLSEYE';
var textOverlay = new YCustomOverlay(yahooMap6.getCenterLatLon());
YUtility.appendNode(textOverlay, labelNode);
yahooMap6.addOverlay(textOverlay);
}
function yahooPlotImage()
{
var image = new YImage("https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvPD-ydg5-zBkX7kgIpop4lssNoO4RsvRM1tikkNWigSzvwOrDMhX0vRXrSvwipd6om4cALIetzl7kC3gQjKcsZ5fif8o6kosOJk_loogTVkQte51GZ6XwsLV_taFqdyoNWlnIlMsqxqI/s400/target.jpg");
var imageOverlay = new YMarker(yahooMap6.getCenterLatLon(), image);
yahooMap6.addOverlay(imageOverlay);
}
</script>
<div>
<p>With the Yahoo Maps API we can:</p>
<ul>
<li>Draw text
<li>Plot an image
<li>Draw a polyline (a line with multiple segments)
</ul>
<p>In the case of the Yahoo Maps API there are a few classes that play a role here:</p>
<ul>
<li>YImage
<ul><li>YImage is not an overlay by itself, but instead defines how an image will be displayed. The image is then supplied as part of the constructor for a YMarker, which plots the image on the map.</ul>
<li>YMarker
<ul><li>Used both for plotting standard points of interest as well as for images.</ul>
<li>YCustomOverlay
<ul><li>An all purpose overlay. Supply whatever HTML you like to this element and it will be plotted on the map at the given point.</ul>
<li>YPolyline
<ul><li>Draws a polyline on the map, with variable line width, color, and opacity.</ul>
</ul>
<p>Missing from this list is the ability to draw a filled polygon. Drawing an unfilled polygon is as simple as using the YPolyline and supplying an the first vertex as the last, thus joining the polygon together. However, if you are looking to have a filled shape, you are out of luck.</p>
<div class="code-yahoo"><pre>
function yahooPlotCircle(scalar)
{
var vertexCount = 64;
var vertices = new Array();
var v = 0;
var radians = Math.PI / 180;
var radiansPerDegree = Math.PI / 180;
var degreesPerVertex = 360 / vertexCount;
var radiansPerVertex = degreesPerVertex * radiansPerDegree;
var center = yahooMap6.getCenterLatLon();
var bounds = yahooMap6.getBoundsLatLon();
var mapHeight = bounds.LatMax - center.Lat;
var mapWidth = bounds.LonMax - center.Lon;
for ( v = 0; v < vertexCount; v++ )
{
radians = v * radiansPerVertex;
vertices.push(
new YGeoPoint(
center.Lat + ( scalar * mapHeight * Math.sin( radians ) ),
center.Lon + ( scalar * mapWidth * Math.cos( radians ) ) ) );
}
vertices.push( vertices[0] );
var circle = new YPolyline(vertices, "#FF0000", 3);
yahooMap6.addOverlay( circle );
}
function yahooPlotLines()
{
var vertices = new Array();
var bounds = yahooMap6.getBoundsLatLon();
var center = yahooMap6.getCenterLatLon();
vertices.push( new YGeoPoint( bounds.LatMax, center.Lon ) );
vertices.push( new YGeoPoint( bounds.LatMin, center.Lon ) );
var verticalLine = new YPolyline( vertices, "#0000FF", 6 );
yahooMap6.addOverlay(verticalLine);
vertices = new Array();
vertices.push( new YGeoPoint( center.Lat, bounds.LonMax ) );
vertices.push( new YGeoPoint( center.Lat, bounds.LonMin ) );
var horizontalLine = new YPolyline( vertices, "#0000FF", 6 );
yahooMap6.addOverlay(horizontalLine);
}
function yahooPlotText()
{
var labelNode = YUtility.createNode('div', 'yahooLabelOverlay');
labelNode.style.color = "#0000FF";
labelNode.style.backgroundColor = "#FFFFFF";
labelNode.innerHTML += 'BULLSEYE';
var textOverlay = new YCustomOverlay(yahooMap6.getCenterLatLon());
YUtility.appendNode(textOverlay, labelNode);
yahooMap6.addOverlay(textOverlay);
}
function yahooPlotImage()
{
var image = new YImage("target.jpg");
var imageOverlay = new YMarker(yahooMap6.getCenterLatLon(), image);
yahooMap6.addOverlay(imageOverlay);
}
</pre></div>
<p>The Yahoo Maps implementation of overlays is a little quirky, especially when it comes to polylines. You may find that your polylines are not drawn properly when changing the zoom level, especially when using a double-click to zoom.</p>
<p>The code necessary to plot text may look a little strange, and that is because it is. The Yahoo Maps API supplies a couple of helper classes, and here we use the YUtility class to add our HTML node that we want to draw as our text overlay. It's a little cumbersome to add to add text in this way, but it does work. I tried using the method of supplying the HTML for the text as a direct parameter to the YCustomOverlay constructor, but this did not appear to work. Your mileage may vary.</p>
</div>
<h3>MapQuest</h3>
<div class="mapDiv" id="mapquestMapDiv6" style="width:640px;height:480px;"></div>
<script type="text/javascript">
var mapquestMap6;
function mapquestInitializer6()
{
mapquestMap6 = new MQA.TileMap(document.getElementById('mapquestMapDiv6'));
mapquestMap6.addControl(new MQA.LargeZoomControl());
mapquestMap6.addControl(new MQA.ViewControl());
mapquestMap6.setCenter(new MQA.LatLng(39.768519, -86.158041), 10);
mapquestPlotLines();
mapquestPlotCircle(0.6);
mapquestPlotCircle(0.5);
mapquestPlotCircle(0.4);
mapquestPlotCircle(0.3);
mapquestPlotCircle(0.2);
mapquestPlotImage();
mapquestPlotText();
}
scriptInitializers.push(mapquestInitializer6);
function mapquestPlotCircle(scalar)
{
var vertexCount = 64;
var vertices = new MQA.LatLngCollection();
var center = mapquestMap6.getCenter();
var bounds = mapquestMap6.getBounds();
var latDiff = scalar * ( center.getLatitude() - bounds.getUpperLeft().getLatitude() );
var latMax = center.getLatitude() + latDiff;
var latMin = center.getLatitude() - latDiff;
var lonDiff = scalar * ( center.getLongitude() - bounds.getUpperLeft().getLongitude() );
var lonMax = center.getLongitude() + lonDiff;
var lonMin = center.getLongitude() - lonDiff;
vertices.add( new MQA.LatLng( latMax, lonMax ) );
vertices.add( new MQA.LatLng( latMin, lonMin ) );
var circle = new MQA.EllipseOverlay();
circle.setValue('shapePoints', vertices);
circle.setValue('fillColor', '#800000');
circle.setValue('fillColorAlpha', 0.2);
circle.setValue('color', '#800000');
circle.setValue('colorAlpha', 0.2);
circle.setValue('borderWidth', 1);
mapquestMap6.addShape( circle );
}
function mapquestPlotLines()
{
var vertices = new MQA.LatLngCollection();
var bounds = mapquestMap6.getBounds();
var center = mapquestMap6.getCenter();
vertices.add( new MQA.LatLng( bounds.getUpperLeft().getLatitude(), center.getLongitude() ) );
vertices.add( new MQA.LatLng( bounds.getLowerRight().getLatitude(), center.getLongitude() ) );
var verticalLine = new MQA.LineOverlay();
verticalLine.setValue('shapePoints', vertices);
verticalLine.setValue('color', '#0000FF');
verticalLine.setValue('borderWidth', 6);
mapquestMap6.addShape(verticalLine);
vertices = new MQA.LatLngCollection();
vertices.add( new MQA.LatLng( center.getLatitude(), bounds.getUpperLeft().getLongitude() ) );
vertices.add( new MQA.LatLng( center.getLatitude(), bounds.getLowerRight().getLongitude() ) );
var horizontalLine = new MQA.LineOverlay();
horizontalLine.setValue('shapePoints', vertices);
horizontalLine.setValue('color', '#0000FF');
horizontalLine.setValue('borderWidth', 6);
mapquestMap6.addShape(horizontalLine);
}
function mapquestPlotText()
{
var textShape = new MQA.Poi(mapquestMap6.getCenter());
textShape.setValue( 'HTMLContent', '<span style="color:#0000FF">BULLSEYE</span>');
mapquestMap6.addShape(textShape);
}
function mapquestPlotImage()
{
var imageShape = new MQA.ImageOverlay();
var vertices = new MQA.LatLngCollection();
vertices.add( mapquestMap6.getCenter() );
var center = mapquestMap6.llToPix( mapquestMap6.getCenter() );
var offset = new MQA.Point( center.getX() + 34, center.getY() + 50 );
vertices.add( mapquestMap6.pixToLL( offset ) );
imageShape.setValue('shapePoints', vertices);
imageShape.setValue('imageURL', 'https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvPD-ydg5-zBkX7kgIpop4lssNoO4RsvRM1tikkNWigSzvwOrDMhX0vRXrSvwipd6om4cALIetzl7kC3gQjKcsZ5fif8o6kosOJk_loogTVkQte51GZ6XwsLV_taFqdyoNWlnIlMsqxqI/s400/target.jpg');
mapquestMap6.addShape(imageShape);
}
</script>
<div>
<p>The MapQuest API allows us to:</p>
<ul>
<li>Draw text
<li>Plot an image
<li>Draw a polyline (a line with multiple segments)
<li>Draw an filled / unfilled polygon
<li>Draw a filled / unfilled ellipse
</ul>
<p>The MapQuest API divides the drawing jobs among several overlay classes:</p>
<ul>
<li>MQA.RectangleOverlay
<ul><li>Draws a rectangle based on two points defining opposite corners of the rectangle.</ul>
<li>MQA.LineOverlay
<ul><li>Draws a multi-segment line (polyline).</ul>
<li>MQA.PolygonOverlay
<ul><li>Draws a polygon, filled or unfilled.</ul>
<li>MQA.EllipseOverlay
<ul><li>Draws an ellipse based on two points defining the opposite corners of a bounding rectangle.</ul>
<li>MQA.ImageOverlay
<ul><li>Draws an image based on two points defining the opposite corners of a bounding rectangle.</ul>
</ul>
<p>The MapQuest API is unique in that it provides special overlays that reduce the number of points needed to define a rectangle and an ellipse. With the other APIs we have had to define our ellipse by creating enough line segments that the shape has the illusion of curvature. The MapQuest API actually draws a curve. Another interesting difference is that when we plot an image using the ImageOverlay, the image will stretch and shrink to cover the same area as we change our zoom level. If this is not the intent, an image can instead be supplied to a MQA.Poi to give the same effect as seen in the other mapping API's. The MapQuest API does not have a specific overlay for text, but text labels can be created by setting the HTMLContent value of a MQA.Poi.</p>
<div class="code-mapquest"><pre>
function mapquestPlotCircle(scalar)
{
var vertexCount = 64;
var vertices = new MQA.LatLngCollection();
var center = mapquestMap6.getCenter();
var bounds = mapquestMap6.getBounds();
var latDiff = scalar * ( center.getLatitude() - bounds.getUpperLeft().getLatitude() );
var latMax = center.getLatitude() + latDiff;
var latMin = center.getLatitude() - latDiff;
var lonDiff = scalar * ( center.getLongitude() - bounds.getUpperLeft().getLongitude() );
var lonMax = center.getLongitude() + lonDiff;
var lonMin = center.getLongitude() - lonDiff;
vertices.add( new MQA.LatLng( latMax, lonMax ) );
vertices.add( new MQA.LatLng( latMin, lonMin ) );
var circle = new MQA.EllipseOverlay();
circle.setValue('shapePoints', vertices);
circle.setValue('fillColor', '#800000');
circle.setValue('fillColorAlpha', 0.2);
circle.setValue('color', '#800000');
circle.setValue('colorAlpha', 0.2);
circle.setValue('borderWidth', 1);
mapquestMap6.addShape( circle );
}
function mapquestPlotLines()
{
var vertices = new MQA.LatLngCollection();
var bounds = mapquestMap6.getBounds();
var center = mapquestMap6.getCenter();
vertices.add( new MQA.LatLng( bounds.getUpperLeft().getLatitude(), center.getLongitude() ) );
vertices.add( new MQA.LatLng( bounds.getLowerRight().getLatitude(), center.getLongitude() ) );
var verticalLine = new MQA.LineOverlay();
verticalLine.setValue('shapePoints', vertices);
verticalLine.setValue('color', '#0000FF');
verticalLine.setValue('borderWidth', 6);
mapquestMap6.addShape(verticalLine);
vertices = new MQA.LatLngCollection();
vertices.add( new MQA.LatLng( center.getLatitude(), bounds.getUpperLeft().getLongitude() ) );
vertices.add( new MQA.LatLng( center.getLatitude(), bounds.getLowerRight().getLongitude() ) );
var horizontalLine = new MQA.LineOverlay();
horizontalLine.setValue('shapePoints', vertices);
horizontalLine.setValue('color', '#0000FF');
horizontalLine.setValue('borderWidth', 6);
mapquestMap6.addShape(horizontalLine);
}
function mapquestPlotText()
{
var textShape = new MQA.Poi(mapquestMap6.getCenter());
textShape.setValue( 'HTMLContent', '<span style="color:#0000FF">BULLSEYE</span>');
mapquestMap6.addShape(textShape);
}
function mapquestPlotImage()
{
var imageShape = new MQA.ImageOverlay();
var vertices = new MQA.LatLngCollection();
vertices.add( mapquestMap6.getCenter() );
var center = mapquestMap6.llToPix( mapquestMap6.getCenter() );
var offset = new MQA.Point( center.getX() + 34, center.getY() + 50 );
vertices.add( mapquestMap6.pixToLL( offset ) );
imageShape.setValue('shapePoints', vertices);
imageShape.setValue('imageURL', 'target.jpg');
mapquestMap6.addShape(imageShape);
}
</pre></div>
</div>
<h3>That is it!</h3>
<div><p>Well, not really. There is a whole lot more to explore in these API's, but with this post we have covered all of the essentials you need to get started. Now it is time to get in there and explore, try some things, and see what you can do. I hope you have found this series informative and helpful. As you start on your own mapping projects, be sure to reference the vendor's development site for full documentation.</p>
<ul>
<li><a href="http://www.microsoft.com/maps/isdk/ajax/">Bing Maps Interactive SDK</a></li>
<li><a href="http://code.google.com/apis/maps/documentation/index.html">Google Maps API</a></li>
<li><a href="http://developer.mapquest.com/web/products/javascript/documentation">MapQuest JavaScript SDK Documentation</a></li>
<li><a href="http://developer.yahoo.com/maps/ajax/">Yahoo! Maps Web Services</a></li>
</ul>
</div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com2tag:blogger.com,1999:blog-8743735145097954046.post-53013341800681514402010-04-07T17:45:00.002-04:002010-04-07T17:46:58.558-04:00Developing with Online Mapping APIs - Part 5: Event Handling<div>
<p>We've covered a lot of ground, and our maps are now able to display rich data to our users. The next step is to allow users to interact with the map and to react to the user's input in a meaningful way. In order to do that we need to be able to handle mapping events. There are three general categories of events:</p>
<ul>
<li>Keyboard and Mouse Events
<li>Map Events
<li>Point of Interest Events
</ul>
<p>Keyboard and mouse events are just what you might think. These events indicate that the mouse has been clicked, double-clicked, or a keyboard key has been pressed. In relation to the map display we can determine at what X and Y position the mouse was clicked.</p>
<p>Mapping events are related to changes in what is displayed. For instance, the user may utilize a control to pan the map north. This would be associated with a particular event. The same is true for changing the zoom or even the type of map that is displayed.</p>
<p>Point of interest events are specific actions taken on a point of interest. If the map displays several POI's, an event may be associated with a click action on an individual POI.</p>
<p>Each mapping API has a slightly different method for handling events. In many cases, a single user action may result in triggering a variety of events.</p>
</div>
<h3>Bing</h3>
<div class="mapDiv" id="bingMapDiv5"></div>
<div>
<div>Interact with the map to see a log of the events that are received.</div>
<div><textarea id="bingEventLog" value="" cols="80" rows="12"></textarea></div>
<div>
<table><tr><td colspan="3">Listen for:</td></tr>
<tr>
<th>Keyboard</th><th>Mouse</th><th>Map</th></tr>
<tr>
<td align="right">onkeypress<input type="checkbox" id="bingOnKeyPress" onclick="bingChangeListeners();" /></td>
<td align="right">onclick<input type="checkbox" id="bingOnClick" onclick="bingChangeListeners();" /></td>
<td align="right">onchangemapstyle<input type="checkbox" id="bingOnChangeMapStyle" onclick="bingChangeListeners();" /></td>
</tr>
<tr>
<td align="right">onkeydown<input type="checkbox" id="bingOnKeyDown" onclick="bingChangeListeners();" /></td>
<td align="right">ondoubleclick<input type="checkbox" id="bingOnDoubleClick" onclick="bingChangeListeners();" /></td>
<td align="right">onchangeview<input type="checkbox" id="bingOnChangeView" onclick="bingChangeListeners();" /></td>
</tr>
<tr>
<td align="right">onkeyup<input type="checkbox" id="bingOnKeyUp" onclick="bingChangeListeners();" /></td>
<td align="right">onmousedown<input type="checkbox" id="bingOnMouseDown" onclick="bingChangeListeners();" /></td>
<td align="right">onendpan<input type="checkbox" id="bingOnEndPan" onclick="bingChangeListeners();" /></td>
</tr>
<tr>
<td> </td>
<td align="right">onmouseup<input type="checkbox" id="bingOnMouseUp" onclick="bingChangeListeners();" /></td>
<td align="right">onendzoom<input type="checkbox" id="bingOnEndZoom" onclick="bingChangeListeners();" /></td>
</tr>
<tr>
<td> </td>
<td align="right">onmouseover<input type="checkbox" id="bingOnMouseOver" onclick="bingChangeListeners();" /></td>
<td align="right">onresize<input type="checkbox" id="bingOnResize" onclick="bingChangeListeners();" /></td>
</tr>
<tr>
<td> </td>
<td align="right">onmouseout<input type="checkbox" id="bingOnMouseOut" onclick="bingChangeListeners();" /></td>
<td align="right">onstartpan<input type="checkbox" id="bingOnStartPan" onclick="bingChangeListeners();" /></td>
</tr>
<tr>
<td> </td>
<td align="right">onmousewheel<input type="checkbox" id="bingOnMouseWheel" onclick="bingChangeListeners();" /></td>
<td align="right">onstartzoom<input type="checkbox" id="bingOnStartZoom" onclick="bingChangeListeners();" /></td>
</tr>
<tr>
<td> </td>
<td align="right">onmousemove<input type="checkbox" id="bingOnMouseMove" onclick="bingChangeListeners();" /></td>
<td> </td>
</tr>
</table>
</div>
<script type="text/javascript">
var bingMap5;
var bingMarker5;
function bingInitializer5()
{
bingMap5 = new VEMap('bingMapDiv5');
bingMap5.SetDashboardSize(VEDashboardSize.Normal);
bingMap5.LoadMap(new VELatLong(39.768519, -86.158041), 13);
bingMap5.ShowDashboard();
bingMarker5 = new VEShape(VEShapeType.Pushpin, new VELatLong(39.768519, -86.158041));
bingMap5.AddShape(bingMarker5);
}
scriptInitializers.push(bingInitializer5);
function bingChangeListeners()
{
// Remove all handlers to start so that we don't end up with a handler registered twice.
bingMap5.DetachEvent("onkeypress", bingEventHandler);
bingMap5.DetachEvent("onkeydown", bingEventHandler);
bingMap5.DetachEvent("onkeyup", bingEventHandler);
bingMap5.DetachEvent("onclick", bingEventHandler);
bingMap5.DetachEvent("ondoubleclick", bingEventHandler);
bingMap5.DetachEvent("onmousemove", bingEventHandler);
bingMap5.DetachEvent("onmousedown", bingEventHandler);
bingMap5.DetachEvent("onmouseup", bingEventHandler);
bingMap5.DetachEvent("onmouseover", bingEventHandler);
bingMap5.DetachEvent("onmouseout", bingEventHandler);
bingMap5.DetachEvent("onmousewheel", bingEventHandler);
var checkBox = document.getElementById("bingOnKeyPress");
if (checkBox.checked)
{
bingMap5.AttachEvent("onkeypress", bingEventHandler);
}
checkBox = document.getElementById("bingOnKeyDown");
if (checkBox.checked)
{
bingMap5.AttachEvent("onkeydown", bingEventHandler);
}
checkBox = document.getElementById("bingOnKeyUp");
if (checkBox.checked)
{
bingMap5.AttachEvent("onkeyup", bingEventHandler);
}
checkBox = document.getElementById("bingOnClick");
if (checkBox.checked)
{
bingMap5.AttachEvent("onclick", bingEventHandler);
}
checkBox = document.getElementById("bingOnDoubleClick");
if (checkBox.checked)
{
bingMap5.AttachEvent("ondoubleclick", bingEventHandler);
}
checkBox = document.getElementById("bingOnMouseMove");
if (checkBox.checked)
{
bingMap5.AttachEvent("onmousemove", bingEventHandler);
}
checkBox = document.getElementById("bingOnMouseDown");
if (checkBox.checked)
{
bingMap5.AttachEvent("onmousedown", bingEventHandler);
}
checkBox = document.getElementById("bingOnMouseUp");
if (checkBox.checked)
{
bingMap5.AttachEvent("onmouseup", bingEventHandler);
}
checkBox = document.getElementById("bingOnMouseOver");
if (checkBox.checked)
{
bingMap5.AttachEvent("onmouseover", bingEventHandler);
}
checkBox = document.getElementById("bingOnMouseOut");
if (checkBox.checked)
{
bingMap5.AttachEvent("onmouseout", bingEventHandler);
}
checkBox = document.getElementById("bingOnMouseWheel");
if (checkBox.checked)
{
bingMap5.AttachEvent("onmousewheel", bingEventHandler);
}
checkBox = document.getElementById("bingOnChangeMapStyle");
if (checkBox.checked)
{
bingMap5.AttachEvent("onchangemapstyle", bingEventHandler);
}
checkBox = document.getElementById("bingOnChangeView");
if (checkBox.checked)
{
bingMap5.AttachEvent("onchangeview", bingEventHandler);
}
checkBox = document.getElementById("bingOnEndPan");
if (checkBox.checked)
{
bingMap5.AttachEvent("onendpan", bingEventHandler);
}
checkBox = document.getElementById("bingOnEndZoom");
if (checkBox.checked)
{
bingMap5.AttachEvent("onendzoom", bingEventHandler);
}
checkBox = document.getElementById("bingOnResize");
if (checkBox.checked)
{
bingMap5.AttachEvent("onresize", bingEventHandler);
}
checkBox = document.getElementById("bingOnStartPan");
if (checkBox.checked)
{
bingMap5.AttachEvent("onstartpan", bingEventHandler);
}
checkBox = document.getElementById("bingOnStartZoom");
if (checkBox.checked)
{
bingMap5.AttachEvent("onstartzoom", bingEventHandler);
}
}
function bingEventHandler(eventDetails)
{
var eventLog = document.getElementById("bingEventLog");
var logEntry = "event received: " + eventDetails.eventName + "\n";
logEntry = logEntry + "\tMap X:\t" + eventDetails.mapX + "\n";
logEntry = logEntry + "\tMap Y:\t" + eventDetails.mapY + "\n";
logEntry = logEntry + "\tZoom:\t" + eventDetails.zoomLevel + "\n";
logEntry = logEntry + "\tElement ID:\t" + eventDetails.elementID + "\n";
eventLog.value = logEntry + eventLog.value;
}
</script>
<div>
<p>The VEMap class exposes an AttachEvent method which is used to subscribe to events. The method accepts two parameters: the name of the event to listen for and a reference to a function that will be called when the event occurs.</p>
<div class="code-bing"><pre>
bingMap5.AttachEvent("ondoubleclick", bingEventHandler);
</pre></div>
<p>The VEMap.DetachEvent method takes the same parameters and will unsubscribe from an event.</p>
<div class="code-bing"><pre>
bingMap5.DetachEvent("ondoubleclick", bingEventHandler);
</pre></div>
<p>The event handling function will be called with a single parameter. The parameter contains the details for the event. If the event is relative to a marker or other shape that is drawn on the map, the elementID field of the event details will contain the ID of the shape. The ID for each shape is maintained by the VEMap instance. Use the VEMap.GetShapeByID method to retrieve a reference to the shape instance.</p>
<p>The mapX and mapY properties indicate where on the map the event took place. These may be null for events that do not have a point on the map. To convert these values to a lattitude and longitude use the VEMap.PixelToLatLong method.</p>
<div class="code-bing"><pre>
function bingEventHandler(eventDetails)
{
var eventLog = document.getElementById("bingEventLog");
var logEntry = "event received: " + eventDetails.eventName + "\n";
logEntry = logEntry + "\tMap X:\t" + eventDetails.mapX + "\n";
logEntry = logEntry + "\tMap Y:\t" + eventDetails.mapY + "\n";
logEntry = logEntry + "\tZoom:\t" + eventDetails.zoomLevel + "\n";
logEntry = logEntry + "\tElement ID:\t" + eventDetails.elementID + "\n";
eventLog.value = logEntry + eventLog.value;
}
</pre></div>
<p>I've only hooked up a portion of the events that are available, and only a selection of the event details are shown on each log entry. Be sure to read through the MSDN documentation on <a href="http://msdn.microsoft.com/en-us/library/bb412543(v=MSDN.10).aspx">Bing VEMap Events</a> for the full set of events and event details. These events allow for you to write code that reacts to the user's actions. In this way you can load data that is within the viewing area when the map is moved, follow the mouse movement across the map, and a variety of other interesting responses.</p>
</div>
<h3>Google</h3>
<div class="mapDiv" id="googleMapDiv5"></div>
<div>Interact with the map to see a log of the events that are received.</div>
<div><textarea id="googleEventLog" rows="12" cols="80"></textarea></div>
<div>
<table><tr><td colspan="3">Listen for:</td></tr>
<tr>
<th>GMap2</th><th>GMarker</th></tr>
<tr>
<td align="right">click<input type="checkbox" id="googleMapClick" onclick="googleChangeListeners();" /></td>
<td align="right">click<input type="checkbox" id="googleMarkerClick" onclick="googleChangeListeners();" /></td>
</tr>
<tr>
<td align="right">dblclick<input type="checkbox" id="googleMapDoubleClick" onclick="googleChangeListeners();" /></td>
<td align="right">dblclick<input type="checkbox" id="googleMarkerDoubleClick" onclick="googleChangeListeners();" /></td>
</tr>
<tr>
<td align="right">movestart<input type="checkbox" id="googleMapMoveStart" onclick="googleChangeListeners();" /></td>
<td> </td>
</tr>
<tr>
<td align="right">move<input type="checkbox" id="googleMapMove" onclick="googleChangeListeners();" /></td>
<td> </td>
</tr>
<tr>
<td align="right">moveend<input type="checkbox" id="googleMapMoveEnd" onclick="googleChangeListeners();" /></td>
<td> </td>
</tr>
<tr>
<td align="right">mouseover<input type="checkbox" id="googleMapMouseOver" onclick="googleChangeListeners();" /></td>
<td align="right">mouseover<input type="checkbox" id="googleMarkerMouseOver" onclick="googleChangeListeners();" /></td>
</tr>
<tr>
<td align="right">mousemove<input type="checkbox" id="googleMapMouseMove" onclick="googleChangeListeners();" /></td>
<td> </td>
</tr>
<tr>
<td align="right">mouseout<input type="checkbox" id="googleMapMouseOut" onclick="googleChangeListeners();" /></td>
<td align="right">mouseout<input type="checkbox" id="googleMarkerMouseOut" onclick="googleChangeListeners();" /></td>
</tr>
<tr>
<td align="right">dragstart<input type="checkbox" id="googleMapDragStart" onclick="googleChangeListeners();" /></td>
<td align="right">dragstart<input type="checkbox" id="googleMarkerDragStart" onclick="googleChangeListeners();" /></td>
</tr>
<tr>
<td align="right">drag<input type="checkbox" id="googleMapDrag" onclick="googleChangeListeners();" /></td>
<td align="right">drag<input type="checkbox" id="googleMarkerDrag" onclick="googleChangeListeners();" /></td>
</tr>
<tr>
<td align="right">dragend<input type="checkbox" id="googleMapDragEnd" onclick="googleChangeListeners();" /></td>
<td align="right">dragend<input type="checkbox" id="googleMarkerDragEnd" onclick="googleChangeListeners();" /></td>
</tr>
<tr>
<td align="right">zoomend<input type="checkbox" id="googleMapZoomEnd" onclick="googleChangeListeners();" /></td>
<td> </td>
</tr>
</table>
</div>
<script type="text/javascript">
var googleMap5;
var googleMarker5;
function googleInitializer5()
{
googleMap5 = new GMap2(document.getElementById("googleMapDiv5"));
googleMap5.setCenter(new GLatLng(39.768519, -86.158041), 13);
googleCurrentMapControlStyle = new GLargeMapControl3D();
googleMap5.addControl(googleCurrentMapControlStyle);
googleMarker5 = new GMarker(new GLatLng(39.768519, -86.158041), {draggable: true});
googleMap5.addOverlay(googleMarker5);
}
scriptInitializers.push(googleInitializer5);
function googleChangeListeners()
{
GEvent.clearInstanceListeners(googleMap5);
GEvent.clearInstanceListeners(googleMarker5);
var checkbox = document.getElementById("googleMapClick");
if ( checkbox.checked )
{
GEvent.addListener(googleMap5, 'click', googleEventHandler);
}
checkbox = document.getElementById('googleMapDoubleClick');
if ( checkbox.checked )
{
GEvent.addListener(googleMap5, 'dblclick', googleEventHandler);
}
checkbox = document.getElementById('googleMapMoveStart');
if ( checkbox.checked )
{
GEvent.addListener(googleMap5, 'movestart', googleEventHandler);
}
checkbox = document.getElementById('googleMapMove');
if ( checkbox.checked )
{
GEvent.addListener(googleMap5, 'move', googleEventHandler);
}
checkbox = document.getElementById('googleMapMoveEnd');
if ( checkbox.checked )
{
GEvent.addListener(googleMap5, 'moveend', googleEventHandler);
}
checkbox = document.getElementById('googleMapDragStart');
if ( checkbox.checked )
{
GEvent.addListener(googleMap5, 'dragstart', googleEventHandler);
}
checkbox = document.getElementById('googleMapDrag');
if ( checkbox.checked )
{
GEvent.addListener(googleMap5, 'drag', googleEventHandler);
}
checkbox = document.getElementById('googleMapDragEnd');
if ( checkbox.checked )
{
GEvent.addListener(googleMap5, 'dragend', googleEventHandler);
}
checkbox = document.getElementById('googleMapMouseOver');
if ( checkbox.checked )
{
GEvent.addListener(googleMap5, 'mouseover', googleEventHandler);
}
checkbox = document.getElementById('googleMapMouseMove');
if ( checkbox.checked )
{
GEvent.addListener(googleMap5, 'mousemove', googleEventHandler);
}
checkbox = document.getElementById('googleMapMouseOut');
if ( checkbox.checked )
{
GEvent.addListener(googleMap5, 'mouseout', googleEventHandler);
}
checkbox = document.getElementById('googleMapZoomEnd');
if ( checkbox.checked )
{
GEvent.addListener(googleMap5, 'zoomend', googleEventHandler);
}
checkbox = document.getElementById('googleMarkerClick');
if ( checkbox.checked )
{
GEvent.addListener(googleMarker5, 'click', googleEventHandler);
}
checkbox = document.getElementById('googleMarkerDoubleClick');
if ( checkbox.checked )
{
GEvent.addListener(googleMarker5, 'dblclick', googleEventHandler);
}
checkbox = document.getElementById('googleMarkerDragStart');
if ( checkbox.checked )
{
GEvent.addListener(googleMarker5, 'dragstart', googleEventHandler);
}
checkbox = document.getElementById('googleMarkerDrag');
if ( checkbox.checked )
{
GEvent.addListener(googleMarker5, 'drag', googleEventHandler);
}
checkbox = document.getElementById('googleMarkerDragEnd');
if ( checkbox.checked )
{
GEvent.addListener(googleMarker5, 'dragend', googleEventHandler);
}
checkbox = document.getElementById('googleMarkerMouseOver');
if ( checkbox.checked )
{
GEvent.addListener(googleMarker5, 'mouseover', googleEventHandler);
}
checkbox = document.getElementById('googleMarkerMouseOut');
if ( checkbox.checked )
{
GEvent.addListener(googleMarker5, 'mouseout', googleEventHandler);
}
}
function googleEventHandler(param1, param2)
{
var eventLog = document.getElementById('googleEventLog');
var eventLogEntry = 'Event fired:\n';
if ( this == googleMap5 )
{
eventLogEntry = eventLogEntry + '\tSource:\tMap\n';
if ( param1 != null )
{
eventLogEntry = eventLogEntry + '\tParam1:\t' + param1 + '\n';
}
if ( param2 != null )
{
eventLogEntry = eventLogEntry + '\tParam2:\t' + param2 + '\n';
}
}
if ( this == googleMarker5 )
{
eventLogEntry = eventLogEntry + '\tSource:\tMarker\n';
if ( param1 != null )
{
eventLogEntry = eventLogEntry + '\tLattitude:\t' + param1.lat() + '\n';
eventLogEntry = eventLogEntry + '\tLongitude:\t' + param1.lng() + '\n';
}
}
eventLog.value = eventLogEntry + eventLog.value;
}
</script>
<div>
<p>The Google API uses static methods on the GEvent class for registering event listeners. This is a similar difference to the Bing implementation as we saw with geocoding. Where the Bing API tends to roll functionality into the VEMap class as a one-stop shop for functionality, the Google API encapsulates functionality in separate classes.</p>
<p>The GEvent.addListener method is used to register a listener for an event. This method takes three parameters: the object that is the source of the event, the name of the event, and a reference to the function to call when the event is fired.</p>
<div class="code-google">
GEvent.addListener(source:Object, event:String, handler:Function)
</div>
<p>Calling this method returns a reference to a GEventListener instance. This reference is used when calling the GEvent.removeListener method to deregister a listener for an event.</p>
</div>
<div class="code-google">
GEvent.removeListener(handle:GEventListener)
</div>
<p>The Google event handlers are much different in invocation than the Bing event handler. The Bing event handler receives single parameter which is of the same type each time. The Google event handlers receive different parameters based on the type of event that was fired. This can make developing a cetralized event handler a bit of a chore. One other noticeable difference is that while Bing gives the X and Y coordinates on the map, the Google event receives lattitude and longitude where applicable.</p>
<div class="code-google"><pre>
function googleEventHandler(param1, param2)
{
var eventLog = document.getElementById('googleEventLog');
var eventLogEntry = 'Event fired:\n';
if ( this == googleMap5 )
{
eventLogEntry = eventLogEntry + '\tSource:\tMap\n';
if ( param1 != null )
{
eventLogEntry = eventLogEntry + '\tParam1:\t' + param1 + '\n';
}
if ( param2 != null )
{
eventLogEntry = eventLogEntry + '\tParam2:\t' + param2 + '\n';
}
}
if ( this == googleMarker5 )
{
eventLogEntry = eventLogEntry + '\tSource:\tMarker\n';
if ( param1 != null )
{
eventLogEntry = eventLogEntry + '\tLattitude:\t' + param1.lat() + '\n';
eventLogEntry = eventLogEntry + '\tLongitude:\t' + param1.lng() + '\n';
}
}
eventLog.value = eventLogEntry + eventLog.value;
}
</pre></div>
<p>A key difference between the Google event handlers and the Bing event handlers is that the JavaScript 'this' reference is set to the source of the event at the time when the event handler is called. This means that where code using the Bing API would need to get the element ID of the VEShape for an event, the Google event handler code can simply use the 'this' reference. It is possible to go one step further by instructing the Google API to bind an event to an object other than the source of the event using the GEvent.bind method.</p>
<div class="code-google">
GEvent.bind(source:Object, event:String, object:Object, method:Function)
</div>
<p>When an event handler is registered in this manner, 'this' will now refer to the object instance given as the third parameter to the function. The bind method returns a reference to a GEventListener just like the addListener method. Unbinding the event handler is achieved by calling the GEvent.removeListener method with a reference to this GEventListener.</p>
<p>Another convenient feature of the Google event system are methods for triggering events without user action, and for clearing all event handlers. The GEvent.trigger method allows for the simulating of events on mapping elements, as well as the introduction of custom events. For instance, if you would like to simulate the user clicking on a marker (and thus fire all of the handlers for that event) you could call:</p>
<div class="code-google">
GEvent.trigger(gmarker, "click");
</div>
<p>This will always fire all of the "click" handlers on teh gmarker instance, rather than attempting to keep track of these references on your own. In addition, you can invent custom events for any element. Simply add your handler using the same GEvent.addListener method with a reference to your custom event name. When you need to fire the custom event, call GEvent.trigger to fire it.</p>
<p>The GEvent class also exposes GEvent.clearListeners and GEvent.clearInstanceListeners. The GEvent.clearListeners method will remove all handlers for a specific event on an element. The GEvent.clearInstanceListeners will remove all handlers for all events.</p>
</div>
<h3>Yahoo</h3>
<div class="mapDiv" id="yahooMapDiv5"></div>
<div>Interact with the map to see a log of the events that are received.</div>
<div><textarea id="yahooEventLog" rows="12" cols="80"></textarea></div>
<div><table><tr><th>YMap</th><th>YMarker</th><th>YGeoRSS</th></tr>
<tr>
<td>endMapDraw</td>
<td>endMapDraw</td>
<td>onEndGeoRSS</td>
</tr>
<tr>
<td>changeZoom</td>
<td>changeZoom</td>
<td> </td>
</tr>
<tr>
<td>startPan</td>
<td>startPan</td>
<td> </td>
</tr>
<tr>
<td>onPan</td>
<td>onPan</td>
<td> </td>
</tr>
<tr>
<td>endPan</td>
<td>endPan</td>
<td> </td>
</tr>
<tr>
<td>startAutoPan</td>
<td>startAutoPan</td>
<td> </td>
</tr>
<tr>
<td>endAutoPan</td>
<td>endAutoPan</td>
<td> </td>
</tr>
<tr>
<td>MouseDown</td>
<td>MouseDown</td>
<td> </td>
</tr>
<tr>
<td>MouseUp</td>
<td>MouseUp</td>
<td> </td>
</tr>
<tr>
<td>MouseClick</td>
<td>MouseClick</td>
<td> </td>
</tr>
<tr>
<td>MouseDoubleClick</td>
<td>MouseDoubleClick</td>
<td> </td>
</tr>
<tr>
<td>MouseOver</td>
<td>MouseOver</td>
<td> </td>
</tr>
<tr>
<td>MouseOut</td>
<td>MouseOut</td>
<td> </td>
</tr>
<tr>
<td>KeyDown</td>
<td>KeyDown</td>
<td> </td>
</tr>
<tr>
<td>KeyUp</td>
<td>KeyUp</td>
<td> </td>
</tr>
<tr>
<td>onEndGeoCode</td>
<td>openExpanded</td>
<td> </td>
</tr>
<tr>
<td>polyLineAdded</td>
<td>closeExpanded</td>
<td> </td>
</tr>
<tr>
<td>polyLineRemoved</td>
<td>openSmartWindow</td>
<td> </td>
</tr>
<tr>
<td>endMapDraw</td>
<td>closeSmartWindow</td>
<td> </td>
</tr>
<tr>
<td>endMapDraw</td>
<td> </td>
<td> </td>
</tr>
<tr>
<td>onEndLocalSearch</td>
<td> </td>
<td> </td>
</tr>
<tr>
<td>onEndTrafficSearch</td>
<td> </td>
<td> </td>
</tr>
</table>
</div>
<script type="text/javascript">
var yahooMap5;
var yahooMarker5;
function yahooInitializer5()
{
yahooMap5 = new YMap(document.getElementById('yahooMapDiv5'));
yahooMap5.addPanControl();
yahooMap5.addTypeControl();
yahooMap5.addZoomLong();
yahooMap5.addZoomScale();
yahooMap5.drawZoomAndCenter(new YGeoPoint(39.768519, -86.158041), 5);
yahooMarker5 = new YMarker(new YGeoPoint(39.768519, -86.158041));
yahooMap5.addOverlay(yahooMarker5);
YEvent.Capture(yahooMap5, EventsList.changeZoom, yahooEventHandler);
YEvent.Capture(yahooMap5, EventsList.startPan, yahooEventHandler);
YEvent.Capture(yahooMap5, EventsList.endPan, yahooEventHandler);
YEvent.Capture(yahooMap5, EventsList.MouseClick, yahooEventHandler);
YEvent.Capture(yahooMap5, EventsList.MouseOver, yahooEventHandler);
YEvent.Capture(yahooMap5, EventsList.MouseOut, yahooEventHandler);
YEvent.Capture(yahooMarker5, EventsList.MouseClick, yahooEventHandler);
YEvent.Capture(yahooMarker5, EventsList.MouseOver, yahooEventHandler);
YEvent.Capture(yahooMarker5, EventsList.MouseOut, yahooEventHandler);
}
scriptInitializers.push(yahooInitializer5);
function yahooEventHandler( eventDetails, clickDetails )
{
var eventLog = document.getElementById('yahooEventLog');
var logEntry = 'Event Received:\n';
if (eventDetails != null)
{
if (eventDetails.thisObj == yahooMap5)
{
logEntry = logEntry + '\Source:\tyahooMap5\n';
}
if (eventDetails.thisObj == yahooMarker5)
{
logEntry = logEntry + '\Source:\tyahooMarker5\n';
}
if (eventDetails.YGeoPoint != null)
{
logEntry = logEntry + '\tLattitude:\t' + eventDetails.YGeoPoint.Lat + '\n';
logEntry = logEntry + '\tLongitude:\t' + eventDetails.YGeoPoint.Lon + '\n';
}
if (eventDetails.zoomObj != null)
{
logEntry = logEntry + '\tZoom Before:\t' + eventDetails.zoomObj.previous + '\n';
logEntry = logEntry + '\tZoom After:\t' + eventDetails.zoomObj.current + '\n';
}
}
if (clickDetails != null)
{
logEntry = logEntry + '\tMouse Lattitude:\t' + clickDetails.Lat + '\n';
logEntry = logEntry + '\tMouse Longitude:\t' + clickDetails.Lon + '\n';
}
eventLog.value = logEntry + eventLog.value;
}
</script>
<div>
<p>The Yahoo Map API uses the YEvent class in much the same way that the Google API exposes the GEvent class. The YEvent class has a single static method, YEvent.Capture, for registering an event handler.</p>
<div class="code-yahoo">
YEvent.Capture(object:[YMap|YMarker], event:EventsList, callback:Function, privateObject:Object)
</div>
<p>As with the GEvent.addListener method, the YEvent.Capture method takes a reference to either a YMap instance or a YMarker instance, the event to listen for, and a reference to the function to call when the event is fired. In addition, the Capture method also accepts an optional object reference that will be passed to the handler when it is fired. Another difference is that the Yahoo Maps API lists all known event types as part of the EventsList object. The full list of available events can be retrieved by calling the YMap.getEventsList() method. The event list can also be found in the documentation for the <a href="http://developer.yahoo.com/maps/ajax/V3.8/index.html#YEvent">YEvent</a> class.</p>
<p>Unlike the Bing and Google maps API's, this is the only method for dealing with events. There is no way to deregister your handler code. Even worse, the Yahoo Maps documentation is silent on what sort of parameters are passed to the event handler for each event type. After some trial and error and snooping through the example code, I've been able to sort out a bit of the story, but it would be much better if Yahoo would make this more clear in their documentation. An event will send either one or two parameters to the event handler. The first parameter contains information on the source of the event (the YMap or the YMarker).</p>
<div class="code-yahoo">
<ul>
<li>YGeoPoint
<ul>
<li>Lat
<li>Lon
</ul>
<li>thisObj
<li>zoomObj
<ul>
<li>current
<li>previous
</ul>
</ul>
</div>
<p>The YGeoPoint is the latitude and longitude as it relates to the event. If this is a click event, it is the location of the click. The second optional parameter has two properties, Lat and Lon, indicating the position of the mouse. As this is not documented, I can't guarantee this will be the behavior for all events, so be sure to experiment with what values you get back in your event handler.</p>
<div class="code-yahoo"><pre>
function yahooEventHandler( eventDetails, clickDetails )
{
var eventLog = document.getElementById('yahooEventLog');
var logEntry = 'Event Received:\n';
if (eventDetails != null)
{
if (eventDetails.thisObj == yahooMap5)
{
logEntry = logEntry + '\Source:\tyahooMap5\n';
}
if (eventDetails.thisObj == yahooMarker5)
{
logEntry = logEntry + '\Source:\tyahooMap5\n';
}
if (eventDetails.YGeoPoint != null)
{
logEntry = logEntry + '\tLattitude:\t' + eventDetails.YGeoPoint.Lat + '\n';
logEntry = logEntry + '\tLongitude:\t' + eventDetails.YGeoPoint.Lon + '\n';
}
if (eventDetails.zoomObj != null)
{
logEntry = logEntry + '\tZoom Before:\t' + eventDetails.zoomObj.previous + '\n';
logEntry = logEntry + '\tZoom After:\t' + eventDetails.zoomObj.current + '\n';
}
}
if (clickDetails != null)
{
logEntry = logEntry + '\tMouse Lattitude:\t' + clickDetails.Lat + '\n';
logEntry = logEntry + '\tMouse Longitude:\t' + clickDetails.Lon + '\n';
}
eventLog.value = logEntry + eventLog.value;
}
</pre></div>
<p>As with the Google event handling, it can be very difficult to determine just what type of event triggered the call. This makes creating a centralized event handler a bit of a chore, so you'll want to create a unique handler for each event type of you want different behavior for each type of event.</p>
</div>
<h3>MapQuest</h3>
<div class="mapDiv" id="mapquestMapDiv5" style="width:640px;height:480px;"></div>
<div>Interact with the map to see a log of the events that are received.</div>
<div><textarea id="mapquestEventLog" rows="12" cols="80"></textarea></div>
<div><table><tr><th>MQA.TileMap</th><th>MQA.Poi</th></tr>
<tr>
<td>click<input type="checkbox" id="mapquestMapClick" onclick="mapquestChangeListeners()"/></td>
<td>click<input type="checkbox" id="mapquestPoiClick" onclick="mapquestChangeListeners()"/></td>
</tr>
<tr>
<td>dblclick<input type="checkbox" id="mapquestMapDoubleClick" onclick="mapquestChangeListeners()"/></td>
<td>dblclick<input type="checkbox" id="mapquestPoiDoubleClick" onclick="mapquestChangeListeners()"/></td>
</tr>
<tr>
<td>mouseup<input type="checkbox" id="mapquestMapMouseUp" onclick="mapquestChangeListeners()"/></td>
<td>mouseup<input type="checkbox" id="mapquestPoiMouseUp" onclick="mapquestChangeListeners()"/></td>
</tr>
<tr>
<td>mousedown<input type="checkbox" id="mapquestMapMouseDown" onclick="mapquestChangeListeners()"/></td>
<td>mousedown<input type="checkbox" id="mapquestPoiMouseDown" onclick="mapquestChangeListeners()"/></td>
</tr>
<tr>
<td>dragstart<input type="checkbox" id="mapquestMapDragStart" onclick="mapquestChangeListeners()"/></td>
<td>mouseover<input type="checkbox" id="mapquestPoiMouseOver" onclick="mapquestChangeListeners()"/></td>
</tr>
<tr>
<td>dragend<input type="checkbox" id="mapquestMapDragEnd" onclick="mapquestChangeListeners()"/></td>
<td>mouseout<input type="checkbox" id="mapquestPoiMouseOut" onclick="mapquestChangeListeners()"/></td>
</tr>
<tr>
<td>drag<input type="checkbox" id="mapquestMapDrag" onclick="mapquestChangeListeners()"/></td>
<td> </td>
</tr>
<tr>
<td>zoomStart<input type="checkbox" id="mapquestMapZoomStart" onclick="mapquestChangeListeners()"/></td>
<td> </td>
</tr>
<tr>
<td>zoomend<input type="checkbox" id="mapquestMapZoomEnd" onclick="mapquestChangeListeners()"/></td>
<td> </td>
</tr>
</table>
</div>
<script type="text/javascript">
var mapquestMap5;
var mapquestMarker5;
function mapquestInitializer5()
{
mapquestMap5 = new MQA.TileMap(document.getElementById('mapquestMapDiv5'));
mapquestMap5.addControl(new MQA.LargeZoomControl());
mapquestMap5.addControl(new MQA.ViewControl());
mapquestMap5.setCenter(new MQA.LatLng(39.768519, -86.158041), 10);
mapquestMarker5 = new MQA.Poi(new MQA.LatLng(39.768519, -86.158041));
mapquestMap5.addShape(mapquestMarker5);
}
scriptInitializers.push(mapquestInitializer5);
function mapquestChangeListeners()
{
MQA.EventManager.clearListeners(mapquestMap5, 'click');
var checkbox = document.getElementById('mapquestMapClick');
if ( checkbox.checked )
{
MQA.EventManager.addListener(mapquestMap5, 'click', mapquestEventHandler);
}
MQA.EventManager.clearListeners(mapquestMap5, 'dblclick');
checkbox = document.getElementById('mapquestMapDoubleClick');
if ( checkbox.checked )
{
MQA.EventManager.addListener(mapquestMap5, 'dblclick', mapquestEventHandler);
}
MQA.EventManager.clearListeners(mapquestMap5, 'mouseup');
checkbox = document.getElementById('mapquestMapMouseUp');
if ( checkbox.checked )
{
MQA.EventManager.addListener(mapquestMap5, 'mouseup', mapquestEventHandler);
}
MQA.EventManager.clearListeners(mapquestMap5, 'mousedown');
checkbox = document.getElementById('mapquestMapMouseDown');
if ( checkbox.checked )
{
MQA.EventManager.addListener(mapquestMap5, 'mousedown', mapquestEventHandler);
}
MQA.EventManager.clearListeners(mapquestMap5, 'dragstart');
checkbox = document.getElementById('mapquestMapDragStart');
if ( checkbox.checked )
{
MQA.EventManager.addListener(mapquestMap5, 'dragstart', mapquestEventHandler);
}
MQA.EventManager.clearListeners(mapquestMap5, 'dragend');
checkbox = document.getElementById('mapquestMapDragEnd');
if ( checkbox.checked )
{
MQA.EventManager.addListener(mapquestMap5, 'dragend', mapquestEventHandler);
}
MQA.EventManager.clearListeners(mapquestMap5, 'drag');
checkbox = document.getElementById('mapquestMapDrag');
if ( checkbox.checked )
{
MQA.EventManager.addListener(mapquestMap5, 'drag', mapquestEventHandler);
}
MQA.EventManager.clearListeners(mapquestMap5, 'zoomstart');
checkbox = document.getElementById('mapquestMapZoomStart');
if ( checkbox.checked )
{
MQA.EventManager.addListener(mapquestMap5, 'zoomstart', mapquestEventHandler);
}
MQA.EventManager.clearListeners(mapquestMap5, 'zoomend');
checkbox = document.getElementById('mapquestMapZoomEnd');
if ( checkbox.checked )
{
MQA.EventManager.addListener(mapquestMap5, 'zoomend', mapquestEventHandler);
}
MQA.EventManager.clearListeners(mapquestMarker5, 'click');
var checkbox = document.getElementById('mapquestPoiClick');
if ( checkbox.checked )
{
MQA.EventManager.addListener(mapquestMarker5, 'click', mapquestEventHandler);
}
MQA.EventManager.clearListeners(mapquestMarker5, 'dblclick');
checkbox = document.getElementById('mapquestPoiDoubleClick');
if ( checkbox.checked )
{
MQA.EventManager.addListener(mapquestMarker5, 'dblclick', mapquestEventHandler);
}
MQA.EventManager.clearListeners(mapquestMarker5, 'mouseup');
checkbox = document.getElementById('mapquestPoiMouseUp');
if ( checkbox.checked )
{
MQA.EventManager.addListener(mapquestMarker5, 'mouseup', mapquestEventHandler);
}
MQA.EventManager.clearListeners(mapquestMarker5, 'mousedown');
checkbox = document.getElementById('mapquestPoiMouseDown');
if ( checkbox.checked )
{
MQA.EventManager.addListener(mapquestMarker5, 'mousedown', mapquestEventHandler);
}
MQA.EventManager.clearListeners(mapquestMarker5, 'mouseover');
checkbox = document.getElementById('mapquestPoiMouseOver');
if ( checkbox.checked )
{
MQA.EventManager.addListener(mapquestMarker5, 'mouseover', mapquestEventHandler);
}
MQA.EventManager.clearListeners(mapquestMarker5, 'mouseout');
checkbox = document.getElementById('mapquestPoiMouseOut');
if ( checkbox.checked )
{
MQA.EventManager.addListener(mapquestMarker5, 'mouseout', mapquestEventHandler);
}
}
function mapquestEventHandler(eventDetails)
{
var eventLog = document.getElementById('mapquestEventLog');
var logEntry = 'Event Received:\n';
if (eventDetails != null)
{
if ( eventDetails.eventName != null )
logEntry = logEntry + '\teventName:\t' + eventDetails.eventName + '\n';
if ( eventDetails.button != null )
logEntry = logEntry + '\tbutton:\t' + eventDetails.button + '\n';
if ( eventDetails.domEvent != null )
logEntry = logEntry + '\tdomEvent:\t' + eventDetails.domEvent + '\n';
if ( eventDetails.poi != null )
logEntry = logEntry + '\tpoi:\t' + eventDetails.poi + '\n';
if ( eventDetails.xy != null )
logEntry = logEntry + '\txy:\t' + eventDetails.xy + '\n';
if ( eventDetails.ll != null )
logEntry = logEntry + '\tll:\t' + eventDetails.ll + '\n';
if ( eventDetails.srcObject != null )
logEntry = logEntry + '\tsrcObject:\t' + eventDetails.srcObject + '\n';
if ( eventDetails.prevZoom != null )
logEntry = logEntry + '\tprevZoom:\t' + eventDetails.prevZoom + '\n';
if ( eventDetails.zoom != null )
logEntry = logEntry + '\tzoom:\t' + eventDetails.zoom + '\n';
if ( eventDetails.clientX != null )
logEntry = logEntry + '\tclientX:\t' + eventDetails.clientX + '\n';
if ( eventDetails.clientY != null )
logEntry = logEntry + '\tclientY:\t' + eventDetails.clientY + '\n';
if ( eventDetails.dragPercentage != null )
logEntry = logEntry + '\tdragPercentage:\t' + eventDetails.dragPercentage + '\n';
if ( eventDetails.dragDirection != null )
logEntry = logEntry + '\tdragDirection:\t' + eventDetails.dragDirection + '\n';
if ( eventDetails.prevMapType != null )
logEntry = logEntry + '\tprevMapType:\t' + eventDetails.prevMapType + '\n';
if ( eventDetails.mapType != null )
logEntry = logEntry + '\tmapType:\t' + eventDetails.mapType + '\n';
}
eventLog.value = logEntry + eventLog.value;
}
</script>
<div>
<p>Hey, we've got our friendly MapQuest map back! Like the Yahoo and Google mapping APIs, the MapQuest API uses a class with static members for hooking up event handlers: MQA.EventManager. The MQA.EventManager has three methods:</p>
<ul>
<li>MQA.EventManager.addListener(source:Object, eventType:String, handler:Function, target:Unknown)
<li>MQA.EventManager.removeListener(source:Object, eventType:String, handler:Function, target:Unknown)
<li>MQA.EventManager.clearListeners(source:Object, eventType:String)
</ul>
<p>When the event handler is called it receives a single parameter of type MQA.Event. Unfortunately, the properties of this class are all set based on the type of event that was fired. Couple that with a lack of documentation surrounding events, and we're back in the same position we were with the Yahoo mapping API. The best source of information I have found is in the <a href="http://developer.mapquest.com/content/documentation/ApiDocumentation/53/JavaScript/JS_DeveloperGuide_v5.3.0.1.htm#styler-id1.18">MapQuest JavaScript API Developer's Guide, section 13 Custom Events</a>. At the end of this section is a list of the events available for map, POI, overlay, and other mapping element types. Interpretting this documentation, we find that the MQA.Event object has the potential to contain the following data (although not all properties will always be populated, so be sure to check for null):</p>
<div class="code-mapquest">
<ul>
<li>MQA.Event.eventName
<ul><li>Name of the event. This will always be present.</ul>
<li>MQA.Event.button
<ul><li>Button associated with the mouse event.</ul>
<li>MQA.Event.domEvent
<ul><li>Name of the DOM event correlating to this event.</ul>
<li>MQA.Event.poi
<ul><li>Reference to the MQA.Poi relevant to this event.</ul>
<li>MQA.Event.xy
<ul><li>Map X,Y coordinates for this event (such as a mouse click).</ul>
<li>MQA.Event.ll
<ul><li>Map Lattitude and Longitude for this event.</ul>
<li>MQA.Event.srcObject
<ul><li>Source of the event.</ul>
<li>MQA.Event.prevZoom
<ul><li>Previous zoom level.</ul>
<li>MQA.Event.zoom
<ul><li>Current zoom level.</ul>
<li>MQA.Event.clientX
<ul><li>X coordinate within the visible portion of the page.</ul>
<li>MQA.Event.clientY
<ul><li>Y coordinate within the visible portion of the page.</ul>
<li>MQA.Event.dragPercentage
<ul><li>(guessing) percentage of the map that was dragged(?)</ul>
<li>MQA.Event.dragDirection
<ul><li>(guessing) Direction the map was dragged(?)</ul>
<li>MQA.Event.prevMapType
<ul><li>MapType in use prior to change in map type.</ul>
<li>MQA.Event.mapType
<ul><li>MapType currently in use.</ul>
</ul>
</div>
<p>Using this in code, we can look for data in our event handler.</p>
<div class="code-mapquest"><pre>
function mapquestEventHandler(eventDetails)
{
var eventLog = document.getElementById('mapquestEventLog');
var logEntry = 'Event Received:\n';
if (eventDetails != null)
{
if ( eventDetails.eventName != null )
logEntry = logEntry + '\teventName:\t' + eventDetails.eventName + '\n';
if ( eventDetails.button != null )
logEntry = logEntry + '\tbutton:\t' + eventDetails.button + '\n';
if ( eventDetails.domEvent != null )
logEntry = logEntry + '\tdomEvent:\t' + eventDetails.domEvent + '\n';
if ( eventDetails.poi != null )
logEntry = logEntry + '\tpoi:\t' + eventDetails.poi + '\n';
if ( eventDetails.xy != null )
logEntry = logEntry + '\txy:\t' + eventDetails.xy + '\n';
if ( eventDetails.ll != null )
logEntry = logEntry + '\tll:\t' + eventDetails.ll + '\n';
if ( eventDetails.srcObject != null )
logEntry = logEntry + '\tsrcObject:\t' + eventDetails.srcObject + '\n';
if ( eventDetails.prevZoom != null )
logEntry = logEntry + '\tprevZoom:\t' + eventDetails.prevZoom + '\n';
if ( eventDetails.zoom != null )
logEntry = logEntry + '\tzoom:\t' + eventDetails.zoom + '\n';
if ( eventDetails.clientX != null )
logEntry = logEntry + '\tclientX:\t' + eventDetails.clientX + '\n';
if ( eventDetails.clientY != null )
logEntry = logEntry + '\tclientY:\t' + eventDetails.clientY + '\n';
if ( eventDetails.dragPercentage != null )
logEntry = logEntry + '\tdragPercentage:\t' + eventDetails.dragPercentage + '\n';
if ( eventDetails.dragDirection != null )
logEntry = logEntry + '\tdragDirection:\t' + eventDetails.dragDirection + '\n';
if ( eventDetails.prevMapType != null )
logEntry = logEntry + '\tprevMapType:\t' + eventDetails.prevMapType + '\n';
if ( eventDetails.mapType != null )
logEntry = logEntry + '\tmapType:\t' + eventDetails.mapType + '\n';
}
eventLog.value = logEntry + eventLog.value;
}
</pre></div>
<p>With the Google and Yahoo map event handlers, we couldn't tell which event had been fired. The MapQuest API will always include the event type in the MQA.Event parameter that is passed to the event handling function. If your goal is to have an event logger, this can be very helpful.</p>
</div>
<h3>Is That It?</h3>
<div><p>While each of the APIs have some similar elements regarding event handling, what became most evident is the quality fo the documentation for each of the mapping APIs. The Yahoo mapping API is simply no help at all when trying to figure out how to handle events. Unless you are only interested in presenting static map information, this could be a real drawback when attempting to create an interactive site. The same is largely true of the MapQuest documentation. The Bing and Google mapping documentation is quite good, and each provides a slightly different method for hooking up events.</p>
<p>With this post we've covered all of the basics for getting a map on your page, displaying some useful information, and allowing for rich interactivity. Now it is time to get creative. In the next post I'll show you how to do some custom drawing which allows you to some very interesting things when presenting information. As always, if you are eager to dive in and learn more, visit the vendor's development site for full documentation.</p>
<ul>
<li><a href="http://www.microsoft.com/maps/isdk/ajax/">Bing Maps Interactive SDK</a></li>
<li><a href="http://code.google.com/apis/maps/documentation/index.html">Google Maps API</a></li>
<li><a href="http://developer.mapquest.com/web/products/javascript/documentation">MapQuest JavaScript SDK Documentation</a></li>
<li><a href="http://developer.yahoo.com/maps/ajax/">Yahoo! Maps Web Services</a></li>
</ul>
</div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0tag:blogger.com,1999:blog-8743735145097954046.post-25376756262101608202010-03-29T23:04:00.001-04:002010-03-29T23:06:21.720-04:00Developing with Online Mapping APIs - Part 4: Geocoding and Reverse-Geocoding<div>
<p>Centering the map and placing points of interest is fairly painless when you know the lattitude and longitude of your POIs, but most people don't think about their location in terms of lat and long. Instead, most people think in terms of addresses. Geocoding is the process of converting an address to lattitude and longitude. Geocoding is a complex process, and for that reason each mapping vendor places limits on the number of geocoding requests that are permitted each day. This number is in the multiple thousands typically, so it will not impact low traffic sites, but for larger sites with many thousands or millions of visitors it can be a concern. There are two methods for coping with this limitation: minimize the necessity of geocoding by storing as much data as possible with a lattitude and longitude reference, or make arrangements with the vendor for additional geocoding requests.</p>
<p>Just as your site visitors are not accustomed to entering locations as lattitude and longitude, they are likewise not accustomed to reading locations as lattitude and longitude. When reading points of interest from the map, the user will prefer to read addresses instead. The process of converting from a lattitude and longitude to a address is called reverse-geocoding. When using geocoding, the address is known, so conversion to a lattitude and longitude is fairly accurate. When reverse-geocoding, however, the requested point may be very far from any known address. For this reason, reverse-geocoding can give some very strange results. The best results with reverse-geocoding are received when the geospatial coordinates to convert are near a road.</p>
<p>When using geocoding or reverse geocoding on your site, be sure to keep your user's privacy in mind. If you will be making the user's location public to others, it is a good idea to allow the user to specify the accuracy of the location. If your app will be taking in a lattitude and longitude from the user's location, consider making only the city, zip code, or state information readable by others rather than a full street and number address.</p>
</div>
<h3>Bing</h3>
<div class="mapDiv" id="bingMapDiv4"></div>
<div>
<div>Click the map anywhere to reverse-geocode that point.</div>
<div>Address To Geocode: <input id="bingAddressToGeocode" type="text" value="Indianapolis, IN" />
<input type="button" value="Geocode" onclick="bingGeocodeLocation();" /></div>
</div>
<script type="text/javascript">
var bingMap4;
var bingMarker4;
function bingInitializer4()
{
bingMap4 = new VEMap('bingMapDiv4');
bingMap4.SetDashboardSize(VEDashboardSize.Normal);
bingMap4.LoadMap(new VELatLong(39.768519, -86.158041), 13);
bingMap4.ShowDashboard();
bingMap4.AttachEvent("onclick", bingReverseGeocodePoint);
}
scriptInitializers.push(bingInitializer4);
function bingGeocodeLocation()
{
var address = document.getElementById('bingAddressToGeocode').value;
bingMap4.Find( null, address, null, null, null, null, null, null, null, null, bingGeocodeLocationCallback );
}
function bingGeocodeLocationCallback( shapeLayer, findResult, places, moreMatchesAvailable, errorInfo )
{
if ( places != null )
{
bingMap4.DeleteAllShapes();
var placeIndex = 0;
for ( placeIndex = 0; placeIndex < places.length; placeIndex++ )
{
var nextShape = new VEShape( VEShapeType.Pushpin, places[placeIndex].LatLong );
bingMap4.AddShape(nextShape);
}
}
}
function bingReverseGeocodePoint(e)
{
var clickLatLng = bingMap4.PixelToLatLong( new VEPixel(e.mapX, e.mapY) );
bingMap4.DeleteAllShapes();
bingMarker4 = new VEShape(VEShapeType.Pushpin, clickLatLng);
bingMap4.AddShape(bingMarker4);
bingMap4.FindLocations( clickLatLng, bingReverseGeocodePointCallback );
}
function bingReverseGeocodePointCallback( possibleAddresses )
{
if ( possibleAddresses != null )
{
var addressIndex = 0;
var addressList = "";
for ( addressIndex = 0; addressIndex < possibleAddresses.length; addressIndex++ )
{
addressList = addressList + "<br>" + possibleAddresses[addressIndex].Name;
}
if (bingMarker4 != null)
{
bingMarker4.SetDescription(addressList);
}
bingMap4.ShowInfoBox(bingMarker4);
}
}
</script>
<div>
<p>Bing uses an overloaded <a href="http://msdn.microsoft.com/en-us/library/bb429645.aspx">VEMap.Find</a> method on the map object for performing geocoding. The Find method can be used to search for a named business or category of business, a street address, a place name, an intersection, or virtually anything else you might think to write into the search bar of the Bing Maps portal. The Find method is tightly coupled with the map display, and as part of the execution of the method can be instructed to add a pin to the map automatically for all matching locations, add the POIs to an existing map layer, and to page results such that only a set number of matches are returned at a time. This flexibility can save you a lot of the overhead of re-centering the map on the proper location, adding the POI marker and other map management. However, this flexibility also makes the Find method a little confusing to use at first. For the purpose of this exercise, we are only looking to turn an address into a lattitude and longitude. For this most basic use of the Find method, the following code will suffice.</p>
<div class="code-bing"><pre>
function bingGeocodeLocation(address)
{
bingMap4.Find( null, address, null, null, null, null, null, null, null, null, bingGeocodeLocationCallback );
}
function bingGeocodeLocationCallback( shapeLayer, findResult, places, moreMatchesAvailable, errorInfo )
{
if ( places != null )
{
bingMap4.DeleteAllShapes();
var placeIndex = 0;
for ( placeIndex = 0; placeIndex < places.length; placeIndex++ )
{
var nextShape = new VEShape( VEShapeType.Pushpin, places[placeIndex].LatLong );
bingMap4.AddShape(nextShape);
}
}
}
</pre></div>
<p>We only needed to supply two parameters for our geocoding call: the address to be geocoded and a callback method to handle the results to the request. The Find method is executed asynchronously. When the call is completed the callback method passed as the last parameter will be called. If the address was found, the places parameter will contain an array of places that match the address. In simple geocoding calls the array will likely have just one element, but code should be prepared to handle multiple matches. The callback to the Find method is non-reentrant, meaning that if another call is made to the Find method before the first call completes, the results to the first Find call will be lost.</p>
<p>Reverse-geocoding is done using the <a href="http://msdn.microsoft.com/en-us/library/cc469978.aspx">VEMap.FindLocations</a> method. Like the Find method this call is performed asynchronously with the last parameter being the callback method to execute when results are found. Unlike the Find method, the FindLocations method has the single purpose of reverse-geocoding a lattitude and longitude. Also, the FindLocations method is only supported in the United States.</p>
<div class="code-bing"><pre>
var bingMarker4;
function bingReverseGeocodePoint(latLongToReverseGeocode)
{
bingMap4.DeleteAllShapes();
bingMarker4 = new VEShape(VEShapeType.Pushpin, latLongToReverseGeocode);
bingMap4.AddShape(bingMarker4);
bingMap4.FindLocations( clickLatLng, bingReverseGeocodePointCallback );
}
function bingReverseGeocodePointCallback( possibleAddresses )
{
if ( possibleAddresses != null )
{
var addressIndex = 0;
var addressList = "";
for ( addressIndex = 0; addressIndex < possibleAddresses.length; addressIndex++ )
{
addressList = addressList + "<br>" + possibleAddresses[addressIndex].Name;
}
if (bingMarker4 != null)
{
bingMarker4.SetDescription(addressList);
}
bingMap4.ShowInfoBox(bingMarker4);
}
}
</pre></div>
<p>If any potential address matches were found for the lattitude and longitude, the parameter to the callback method will be non-null. The parameter is an array of type <a href="http://msdn.microsoft.com/en-us/library/bb429615.aspx">VEPlace</a>. The Name property of each VEPlace gives a candidate address that the lattitude and longitude might resolve to.</p>
</div>
<h3>Google</h3>
<div class="mapDiv" id="googleMapDiv4"></div>
<div>
<div>Click the map anywhere to reverse-geocode that point.</div>
<div>Address To Geocode: <input id="googleAddressToGeocode" type="text" value="Indianapolis, IN" />
<input type="button" value="Geocode" onclick="googleGeocodeLocation();" /></div>
</div>
<script type="text/javascript">
var googleMap4;
var googleMarker4;
var googleGeocoder = new GClientGeocoder();
function googleInitializer4()
{
googleMap4 = new GMap2(document.getElementById("googleMapDiv4"));
googleMap4.setCenter(new GLatLng(39.768519, -86.158041), 13);
googleCurrentMapControlStyle = new GLargeMapControl3D();
googleMap4.addControl(googleCurrentMapControlStyle);
GEvent.addListener(googleMap4, "click", googleReverseGeocodePoint );
}
scriptInitializers.push(googleInitializer4);
function googleGeocodeLocation()
{
var addressToGeocode = document.getElementById('googleAddressToGeocode').value;
googleGeocoder.getLatLng(addressToGeocode, googleGeocodeLocationCallback);
}
function googleGeocodeLocationCallback( place )
{
if ( place != null )
{
googleMap4.clearOverlays();
googleMap4.setCenter(place);
googleMap4.addOverlay(new GMarker(place));
}
}
function googleReverseGeocodePoint(overlay, latlng)
{
googleMap4.clearOverlays();
googleMarker4 = new GMarker(latlng);
googleMap4.addOverlay(googleMarker4);
googleGeocoder.getLocations(latlng, googleReverseGeocodePointCallback);
}
function googleReverseGeocodePointCallback( response )
{
if ( response != null && response.Status.code == 200 )
{
var addressIndex = 0;
var addressList = "<div class='googleInfoWindow'><ul>";
for ( addressIndex = 0; addressIndex < response.Placemark.length; addressIndex++ )
{
addressList = addressList + "<li>" + response.Placemark[addressIndex].address;
}
addressList = addressList + "</ul>";
if (googleMarker4 != null)
{
googleMarker4.openInfoWindowHtml(addressList);
}
}
}
</script>
<div>
<p>Unlike Bing, Google provides Geocoding as an independent web service. The GClientGeocoder object exposes a getLatLng method that takes a string parameter containing the address to geocode, and a reference to a callback function. The callback function has one parameter, the GLatLng of the geocoded address. If the address could not be geocoded this value will be null.</p>
<p>The GClientGeocoder object has no information about any maps on your page. This leaves you to write the code that will act on the geocoded address. This means a bit more code for you if you want to center the map and mark the location, but it also means the geocoding method is clean and fast. In addition, the GClientGeocoder can utilize a cache that keeps a client side mapping of locations to addresses. This can greatly speed up execution when the same address could be geocoded again and again from the site.</p>
<div class="code-google"><pre>
function googleGeocodeLocation(addressToGeocode)
{
googleGeocoder.getLatLng(addressToGeocode, googleGeocodeLocationCallback);
}
function googleGeocodeLocationCallback( place )
{
if ( place != null )
{
googleMap4.clearOverlays();
googleMap4.setCenter(place);
googleMap4.addOverlay(new GMarker(place));
}
}
</pre></div>
<p>In addition to the getLatLng method, the getLocations method can be used to lookup a lattitude and longitude for an address. The difference between getLatLng and getLocations is that the getLocations method will call the callback function with a JSON object with richer information from the geocoding execution. The JSON data is defined as follows:
</p>
<div class="code-google">
<ul>
<li>name
<li>Status
<ul>
<li>code
<li>request
</ul>
<li>Placemark
<ul>
<li>address
<li>AddressDetails
<ul>
<li>Country
<ul>
<li>CountryNameCode
<li>AdministrativeArea
<ul>
<li>AdministrativeAreaName
<li>SubAdministrativeArea
<ul>
<li>SubAdministrativeAreaName
<li>Locality
<ul>
<li>LocalityName
<li>Thoroughfare
<ul>
<li>ThoroughfareName
</ul>
<li>PostalCode
<ul>
<li>PostalCodeNumber
</ul>
</ul>
</ul>
</ul>
</ul>
<li>Accuracy
</ul>
<li>Point
<ul>
<li>coordinates
</ul>
</ul>
</ul>
</div>
<p>That is a whole lot of data. Fortunately, for the purpose of geocoding, we are only interested in the coordinates. Notice that using this method for geocoding allows for multiple results to be returned. This is rare, but possible.</p>
<p>The GClientGeocoder class also performs reverse-geocoding using the same getLocations method. Rather than passing a string to this method, pass a GLatLng. The same JSON content will be returned, but in this case our interest will be in the AddressDetails data. Each nesting level gives us a more specific form of the address. The geocoder / reverse-geocoder does not guarantee accuracy, nor that all of this data be populated.</p>
<div class="code-google"><pre>
function googleReverseGeocodePoint(latlng)
{
googleMap4.clearOverlays();
googleMarker4 = new GMarker(latlng);
googleMap4.addOverlay(googleMarker4);
googleGeocoder.getLocations(latlng, googleReverseGeocodePointCallback);
}
function googleReverseGeocodePointCallback( response )
{
if ( response != null && response.Status.code == 200 )
{
var addressIndex = 0;
var addressList = "<div class='googleInfoWindow'><ul>";
for ( addressIndex = 0; addressIndex < response.Placemark.length; addressIndex++ )
{
addressList = addressList + "<li>" + response.Placemark[addressIndex].address;
}
addressList = addressList + "</ul>";
if (googleMarker4 != null)
{
googleMarker4.openInfoWindowHtml(addressList);
}
}
}
</pre></div>
<p>A little side discussion here. You might look at that code and think a couple of things. One, "Hey, he used an Info Window, and we haven't seen that yet for the Google API." Well...so I cheated. Anyway, what you see there for the Info Window is about all you need to know. You can open one using the openInfoWindowHtml method on a GMarker, and the parameter can be just about any HTML you like. The second thing you might be wondering is why I set the class to 'googleInfoWindow'. When I first coded this up and ran it on the page, the Info Window was blank. Turns out, the the body color for text was being used for the text within the Info Window. White on white wasn't showing up so well, so I'm using the 'googleInfoWindow' class to be sure that my text is printed in black.</p>
</div>
<h3>Yahoo</h3>
<div class="mapDiv" id="yahooMapDiv4"></div>
<div>
<div>Address To Geocode: <input id="yahooAddressToGeocode" type="text" value="Indianapolis, IN" />
<input type="button" value="Geocode" onclick="yahooGeocodeLocation();" /></div>
</div>
<script type="text/javascript">
var yahooMap4;
var yahooPOIs = new Array();
function yahooInitializer4()
{
yahooMap4 = new YMap(document.getElementById('yahooMapDiv4'));
yahooMap4.addPanControl();
yahooMap4.addTypeControl();
yahooMap4.addZoomLong();
yahooMap4.addZoomScale();
yahooMap4.drawZoomAndCenter(new YGeoPoint(39.768519, -86.158041), 5);
YEvent.Capture(yahooMap4, EventsList.onEndGeoCode, yahooOnEndGeoCodeListener);
}
scriptInitializers.push(yahooInitializer4);
function yahooGeocodeLocation()
{
var addressToGeocode = document.getElementById('yahooAddressToGeocode').value;
yahooMap4.geoCodeAddress(addressToGeocode);
}
function yahooOnEndGeoCodeListener(result)
{
if ( result != null )
{
yahooMap4.removeMarkersAll();
var geocodedPoint = new YGeoPoint(result.GeoPoint.Lat, result.GeoPoint.Lon);
yahooMap4.panToLatLon(geocodedPoint);
yahooMap4.addMarker(geocodedPoint);
}
}
</script>
<div>
<p>Like the Bing API, the Yahoo! Maps API exposes geocoding functionality via a method on the map class: YMap.geoCodeAddress. This method takes just one parameter, which is the address to geocode.</p>
<div class="code-yahoo"><pre>
function yahooGeocodeLocation(addressToGeocode)
{
yahooMap4.geoCodeAddress(addressToGeocode);
}
</pre></div>
<p>The geocoded result is returned by the map instance firing the onEndGeoCode event (leading me to believe that I should have covered events before getting into geocoding, oh well). In order to get the result of the geocoding request, the code must register a handler for the onEndGeoCode event. This is done by using the YEvent.Capture method, which takes three parameters: the event source, the event to listen for, and the function to call when the event fires.</p>
<div class="code-yahoo"><pre>
function yahooInitializer4()
{
yahooMap4 = new YMap(document.getElementById('yahooMapDiv4'));
yahooMap4.addPanControl();
yahooMap4.addTypeControl();
yahooMap4.addZoomLong();
yahooMap4.addZoomScale();
yahooMap4.drawZoomAndCenter(new YGeoPoint(39.768519, -86.158041), 5);
YEvent.Capture(yahooMap4, EventsList.onEndGeoCode, yahooOnEndGeoCodeListener);
}
</pre></div>
<p>The callback method receives a single parameter containing the geocoding result, if the execution succeeded. Unfortunately the Yahoo! Maps AJAX Web Services documentation does not provide a definition for the parameter that is returned. From what I can tell by inspecting the value, the parameter is defined as shown below. Note, there is no guarantee that this is correct, or that Yahoo! will continue to use this definition, so be sure to verify the response when implementing your own code.</p>
<div class="code-yahoo">
<ul>
<li>eventObjectGeoCode
<ul>
<li>Address
<li>GeoPoint
<ul>
<li>Lat
<li>Lon
</ul>
<li>ThisMap
<li>success
</ul>
</ul>
</div>
<p>The Address is a string containing the text address that was sent to the geocoder. The GeoPoint field contains the lattitude and longitude the address resolved to. The ThisMap field contains a reference back to the map that raised the event, and the success field indicates if the geocoding execution succeeded (1 == success). The method for getting geocoding results with Yahoo! Maps is unique in that multiple listeners can be attached to the event for completion of geocoding.</p>
<div class="code-yahoo"><pre>
function yahooOnEndGeoCodeListener(result)
{
if ( result != null )
{
yahooMap4.removeMarkersAll();
var geocodedPoint = new YGeoPoint(result.GeoPoint.Lat, result.GeoPoint.Lon);
yahooMap4.panToLatLon(geocodedPoint);
yahooMap4.addMarker(geocodedPoint);
}
}
</pre></div>
<p>The Yahoo! Maps API does not provide a mechanism for reverse geocoding.</p>
</div>
<h3>MapQuest</h3>
<div>
<p>Hmmmm, no map here. That can't be good. MapQuest offers a Geocoding and Reverse-Geocoding web service separately from the JavaScript mapping API. It is a RESTful web service, and works quite well. Unfortunately, in order to use the RESTful geocoding and reverse-geocoding web services provided by MapQuest you must create a proxy on your server to access them. This is to avoid cross domain scripting (your JavaScript code may not make calls to servers it was not loaded from). Previous versions of the MapQuest API did include a MQGeocode class that allowed for geocoding as part of the MapQuest JavaScript API, but that class is not part of the current API. All hope is not lost, however. In the beta for version 6.0 of the MapQuest API the geocoding function makes a triumphant return in the form of MQA.Geocoder.</p>
<p>Creating a JavaScript proxy to be hosted on your server for the purpose of accessing the RESTful geocoding services provided by MapQuest is beyond the scope of this series.</p>
<p>
</div>
<h3>Is That It?</h3>
<div><p>This post highlighted some pretty drastic differences between the APIs provided by each vendor. Bing provides a highly flexible method for geocoding that is tightly coupled to the map. Google provides a clean and fast geocoding mechanism that is independent of the map. Yahoo provides base geocoding services but no reverse-geocoding. Finally, MapQuest requires the implementation of a JavaScript proxy in order to even access their geocoding services. As part of working with geocoding services we've also touched a bit on event handling in each API, as well as the Info Window provided by the Google API. Next, we'll take a more complete look at how to handle events in for each mapping system. As always, if you are eager to learn more just follow these links to the vendor specific API documentation.</p>
<ul>
<li><a href="http://www.microsoft.com/maps/isdk/ajax/">Bing Maps Interactive SDK</a></li>
<li><a href="http://code.google.com/apis/maps/documentation/index.html">Google Maps API</a></li>
<li><a href="http://developer.mapquest.com/web/products/javascript/documentation">MapQuest JavaScript SDK Documentation</a></li>
<li><a href="http://developer.yahoo.com/maps/ajax/">Yahoo! Maps Web Services</a></li>
</ul>
</div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0tag:blogger.com,1999:blog-8743735145097954046.post-57317439424217233692010-03-07T23:51:00.006-05:002010-03-14T23:56:51.815-04:00Developing with Online Mapping APIs - Part 3: Marking Points of Interest<div>
<p>So far we've put together a page that has a map displayed, centered and zoomed on our location, and with the controls displayed as we like. The next step is to mark points of interest on our map. This is one method of providing location based information to the user. A point of interest marks a location on the map, and can provide additional details about that location.</p>
<p>When working with points of interest, there are a couple of things we want to do. First, we want to determine where the Point of Interest (POI) will be marked on the map. Second, we want to know how it will be marked. In some cases there are a variety of default market types to choose from. Next, we need to consider how to handle when the map contains many POIs, and when many POIs overlap on or near the same position. In the section below we will walk through each mapping API and how these concerns are addressed.</p>
</div>
<h3>Bing</h3>
<div class="mapDiv" id="bingMapDiv3"></div>
<div>
<div>Lattitude: <input id="bingLat" type="text" value="39.768519" /></div>
<div>Longitude: <input id="bingLng" type="text" value="-86.158041" /></div>
<div>Information Title: <input id="bingInfoTitle" type="text" value="Title" /></div>
<div>Information Description: <input id="bingInfoDescription" type="text" value="Description" /></div>
<div><input type="button" value="Add POI" onclick="addBingPOI();"></div>
</div>
<div>
<select id="bingPOISelect">
<option>Select a POI to Remove</option>
</select>
<input type="button" value="Remove POI" onclick="removeBingPOI();"/>
</div>
<script type="text/javascript">
var bingMap3;
var bingPOIs = new Array();
function bingInitializer3()
{
bingMap3 = new VEMap('bingMapDiv3');
bingMap3.SetDashboardSize(VEDashboardSize.Normal);
bingMap3.LoadMap(new VELatLong(39.768519, -86.158041), 13);
bingMap3.ShowDashboard();
}
scriptInitializers.push(bingInitializer3);
function addBingPOI()
{
var title = document.getElementById('bingInfoTitle');
var description = document.getElementById('bingInfoDescription');
var lat = document.getElementById('bingLat');
var lng = document.getElementById('bingLng');
var newPOI = new VEShape(VEShapeType.Pushpin, new VELatLong(parseFloat(lat.value), parseFloat(lng.value)));
newPOI.SetTitle(title.value);
newPOI.SetDescription(description.value);
bingMap3.AddShape(newPOI);
var bingPOISelect = document.getElementById('bingPOISelect');
var newPOIOption = document.createElement('option');
newPOIOption.text = title.value;
var poiIndex = 0;
for ( poiIndex = 0; poiIndex < bingPOIs.length; poiIndex++ )
{
if ( bingPOIs[poiIndex] == null )
{
newPOIOption.value = poiIndex;
bingPOISelect.add(newPOIOption);
bingPOIs[poiIndex] = newPOI;
return;
}
}
newPOIOption.value = bingPOIs.length;
bingPOISelect.add(newPOIOption);
bingPOIs.push(newPOI);
}
function removeBingPOI()
{
var bingPOISelect = document.getElementById('bingPOISelect');
var poiIndex = parseInt(bingPOISelect.value);
if (typeof poiIndex == 'number')
{
bingMap3.DeleteShape(bingPOIs[poiIndex]);
bingPOIs[poiIndex] = null;
bingPOISelect.remove(bingPOISelect.selectedIndex);
}
}
</script>
<div>
<p>Adding a POI to a Bing map requires creating an instance of the VEShape class and then calling the VEMap.AddShape method.
<div class="code-bing">
<ul>
<li>VEShape(VEShapeType, VELatLng)
<li>VEMap.AddShape(VEShape)
<li>VEMap.DeleteShape(VEShape)
<li>VEMap.DeleteAllShapes()
</ul>
</div>
<p>The VEShape constructor takes a VEShapeType and the VELatLng referencing the location for the POI. For this example, we are only concerned with the VEShapeType.Pushpin (we'll discuss Polyline and Polygon later). There are two ways to remove the POI once it has been added to the map. If a reference is maintained to the POI, it may be later removed using the VEMap.DeleteShape method. Otherwise, all POIs may be removed at once by using the VEMap.DeleteAllShapes method. Managing the list of POIs can be a bit cumbersome, but it allows for more efficient use of the API.</p>
<p>The VEShape class has two other methods that are worth discussing here. The VEShape.SetTitle and VEShape.SetDescription methods can be used to provide details about what the POI is marking. When the mouse is hovered over the POI the title and description will be displayed to the user.</p>
<p>Putting all of this together, here is the sample code used on this page for adding and removing POIs to the page.</p>
<div class="code-bing"><pre>
<div class="mapDiv" id="bingMapDiv3"></div>
<div>
<div>Lattitude: <input id="bingLat" type="text" value="39.768519" /></div>
<div>Longitude: <input id="bingLng" type="text" value="-86.158041" /></div>
<div>Information Title: <input id="bingInfoTitle" type="text" value="Title" /></div>
<div>Information Description: <input id="bingInfoDescription" type="text" value="Description" /></div>
<div><input type="button" value="Add POI" onclick="addBingPOI();"></div>
</div>
<div>
<select id="bingPOISelect">
<option>Select a POI to Remove</option>
</select>
<input type="button" value="Remove POI" onclick="removeBingPOI();"/>
<script type="text/javascript">
var bingMap3;
var bingPOIs = new Array();
function bingInitializer3()
{
bingMap3 = new VEMap('bingMapDiv3');
bingMap3.SetDashboardSize(VEDashboardSize.Normal);
bingMap3.LoadMap(new VELatLong(39.768519, -86.158041), 13);
bingMap3.ShowDashboard();
}
scriptInitializers.push(bingInitializer3);
function addBingPOI()
{
var title = document.getElementById('bingInfoTitle');
var description = document.getElementById('bingInfoDescription');
var lat = document.getElementById('bingLat');
var lng = document.getElementById('bingLng');
var newPOI = new VEShape(VEShapeType.Pushpin, new VELatLong(parseFloat(lat.value), parseFloat(lng.value)));
newPOI.SetTitle(title.value);
newPOI.SetDescription(description.value);
bingMap3.AddShape(newPOI);
var bingPOISelect = document.getElementById('bingPOISelect');
var newPOIOption = document.createElement('option');
newPOIOption.text = title.value;
var poiIndex = 0;
for ( poiIndex = 0; poiIndex < bingPOIs.length; poiIndex++ )
{
if ( bingPOIs[poiIndex] == null )
{
newPOIOption.value = poiIndex;
bingPOISelect.add(newPOIOption);
bingPOIs[poiIndex] = newPOI;
return;
}
}
newPOIOption.value = bingPOIs.length;
bingPOISelect.add(newPOIOption);
bingPOIs.push(newPOI);
}
function removeBingPOI()
{
var bingPOISelect = document.getElementById('bingPOISelect');
var poiIndex = parseInt(bingPOISelect.value);
if (typeof poiIndex == 'number')
{
bingMap3.DeleteShape(bingPOIs[poiIndex]);
bingPOIs[poiIndex] = null;
bingPOISelect.remove(bingPOISelect.selectedIndex);
}
}
</script>
</pre></div>
<p>These methods are fine when dealing with a handful of points of interest, but do not scale when there are many points of interest, or a variety of points of interest of different types. You may want to be able to switch on a set of points of interest or switch them off at any time. Also, you may have many points of interest that are clustered very closely together. How can we manage scaling in this way?</p>
<p>One way of dealing with scaling issues is to use the VEShape.SetMinZoomLevel and VEShape.SetMaxZoomLevel methods. These methods prevent the POI from being displayed at zoom levels that are too high or too low.</p>
<p>Another way of dealing with scaling issues is to use a VEShapeLayer. A VEShapeLayer is a way to manage a collection of VEShape instances. You might create a layer to represent a particular type. For instance, you might have one VEShapeLayer for food POIs, one for fuel POIs, and one for hotel POIs. In this way you can switch these POIs on or off all at once. A VEShapeLayer is added and removed to the map with similar methods to what we used for VEShape:</p>
<div class="code-bing">
<ul>
<li>VEShapeLayer()
<li>VEShapeLayer.AddShape(VEShape)
<li>VEShapeLayer.DeleteShape(VEShape)
<li>VEShapeLayer.DeleteAllShapes()
<li>VEMap.AddShapeLayer(VEShapeLayer)
<li>VEMap.DeleteShapeLayer(VEShapeLayer)
<li>VEMap.DeleteAllShapeLayers()
</ul>
</div>
<p>In addition, the VEShapeLayer object provides a mechanism for managing VEShape instances that are tightly clustered together. The VEShapeLayer.SetClusteringConfiguration method accepts a type of either VEClusteringType.None for no clustering or VEClusteringType.Grid which uses a simple clustering algorithm.</p>
</div>
<h3>Google</h3>
<div class="mapDiv" id="googleMapDiv3"></div>
<div>
<div>Lattitude: <input id="googleLat" type="text" value="39.768519" /></div>
<div>Longitude: <input id="googleLng" type="text" value="-86.158041" /></div>
<div>Information Title: <input id="googleInfoTitle" type="text" value="Title" /></div>
<div><input type="button" value="Add POI" onclick="addGooglePOI();"></div>
</div>
<div>
<select id="googlePOISelect">
<option>Select a POI to Remove</option>
</select>
<input type="button" value="Remove POI" onclick="removeGooglePOI();"/>
</div>
<script type="text/javascript">
var googleMap3;
var googlePOIs = new Array();
function googleInitializer3()
{
googleMap3 = new GMap2(document.getElementById("googleMapDiv3"));
googleMap3.setCenter(new GLatLng(39.768519, -86.158041), 13);
googleCurrentMapControlStyle = new GLargeMapControl3D();
googleMap3.addControl(googleCurrentMapControlStyle);
}
scriptInitializers.push(googleInitializer3);
function addGooglePOI()
{
var title = document.getElementById('googleInfoTitle');
var lat = document.getElementById('googleLat');
var lng = document.getElementById('googleLng');
var newPOI = new GMarker(new GLatLng(parseFloat(lat.value), parseFloat(lng.value)), { title: title.value });
googleMap3.addOverlay(newPOI);
var googlePOISelect = document.getElementById('googlePOISelect');
var newPOIOption = document.createElement('option');
newPOIOption.text = title.value;
var poiIndex = 0;
for ( poiIndex = 0; poiIndex < googlePOIs.length; poiIndex++ )
{
if ( googlePOIs[poiIndex] == null )
{
newPOIOption.value = poiIndex;
googlePOISelect.add(newPOIOption);
googlePOIs[poiIndex] = newPOI;
return;
}
}
newPOIOption.value = googlePOIs.length;
googlePOISelect.add(newPOIOption);
googlePOIs.push(newPOI);
}
function removeGooglePOI()
{
var googlePOISelect = document.getElementById('googlePOISelect');
var poiIndex = parseInt(googlePOISelect.value);
if (typeof poiIndex == 'number')
{
googleMap3.removeOverlay(googlePOIs[poiIndex]);
googlePOIs[poiIndex] = null;
googlePOISelect.remove(googlePOISelect.selectedIndex);
}
}
</script>
<div>
<p>Adding a POI to a Google map requires creating an instance of the GMarker class and then calling the GMap2.addOverlay method.</p>
<div class="code-google">
<ul>
<li>GMarker(GLatLng)
<li>GMap2.addOverlay(GOverlay)
<li>GMap2.removeOverlay(GOverlay)
<li>GMap2.clearOverlays()
</ul>
</div>
<p>The GMarker class derives from the GOverlay class. The constructor takes a GLatLng referencing the location for the POI. There are two ways to remove the POI once it has been added to the map. If a reference is maintained to the POI, it may be later removed using the GMap2.removeOverlay method. Otherwise, all POIs may be removed at once by using the GMap2.clearOverlays method.</p>
<p>We saw in the Bing map that a Title and Description can be supplied that will be displayed when the mouse hovers over the POI. Google Maps handles this a bit differently. The constructor for the GMarker can also accept a second parameter of type GMarkerOptions. The GMarkerOptions class has a title property that contains the text to be displayed as a tooltip when the mouse hovers over the POI.</p>
<p>Putting all of this together, here is the sample code used on this page for adding and removing POIs to the page.</p>
<div class="code-google"><pre>
<div class="mapDiv" id="googleMapDiv3"></div>
<div>
<div>Lattitude: <input id="googleLat" type="text" value="39.768519" /></div>
<div>Longitude: <input id="googleLng" type="text" value="-86.158041" /></div>
<div>Information Title: <input id="googleInfoTitle" type="text" value="Title" /></div>
<div><input type="button" value="Add POI" onclick="addGooglePOI();"></div>
</div>
<div>
<select id="googlePOISelect">
<option>Select a POI to Remove</option>
</select>
<input type="button" value="Remove POI" onclick="removeGooglePOI();"/>
<script type="text/javascript">
var googleMap3;
function googleInitializer3()
{
googleMap3 = new GMap2(document.getElementById("googleMapDiv3"));
googleMap3.setCenter(new GLatLng(39.768519, -86.158041), 13);
googleCurrentMapControlStyle = new GLargeMapControl3D();
googleMap3.addControl(googleCurrentMapControlStyle);
}
scriptInitializers.push(googleInitializer3);
function addGooglePOI()
{
var title = document.getElementById('googleInfoTitle');
var lat = document.getElementById('googleLat');
var lng = document.getElementById('googleLng');
var newPOI = new GMarker(new GLatLng(parseFloat(lat.value), parseFloat(lng.value)), { title: title.value });
googleMap3.addOverlay(newPOI);
var googlePOISelect = document.getElementById('googlePOISelect');
var newPOIOption = document.createElement('option');
newPOIOption.text = title.value;
var poiIndex = 0;
for ( poiIndex = 0; poiIndex < googlePOIs.length; poiIndex++ )
{
if ( googlePOIs[poiIndex] == null )
{
newPOIOption.value = poiIndex;
googlePOISelect.add(newPOIOption);
googlePOIs[poiIndex] = newPOI;
return;
}
}
newPOIOption.value = googlePOIs.length;
googlePOISelect.add(newPOIOption);
googlePOIs.push(newPOI);
}
function removeGooglePOI()
{
var googlePOISelect = document.getElementById('googlePOISelect');
var poiIndex = parseInt(googlePOISelect.value);
if (typeof poiIndex == 'number')
{
googleMap3.removeOverlay(googlePOIs[poiIndex]);
googlePOIs[poiIndex] = null;
googlePOISelect.remove(googlePOISelect.selectedIndex);
}
}
</script>
</pre></div>
<p>As before, these methods are fine when dealing with a handful of points of interest, but do not scale when there are many points of interest, or a variety of points of interest of different types. The GMarkerManager class is available to handle hundreds of markers in an efficient manner. GMarker instances are added in groups to the GMarkerManager, and the appropriate maximum and minimum zoom levels at which those POIs should be visible is set as part of that call. The GMarkerManager has been marked as deprecated at this time with Google recommending the use of the open source <a href="http://code.google.com/p/gmaps-utility-library-dev/">MarkerManager</a> API instead. As this series focuses on the vanilla, non-beta versions of each vendor's API, I will not be covering that library here. The Google Maps API does not have an equivalent of the VEShapeLayer class found in the Bing API.</p>
</div>
<h3>Yahoo</h3>
<div class="mapDiv" id="yahooMapDiv3"></div>
<div>
<div>Lattitude: <input id="yahooLat" type="text" value="39.768519" /></div>
<div>Longitude: <input id="yahooLng" type="text" value="-86.158041" /></div>
<div>Information Title: <input id="yahooInfoTitle" type="text" value="Title" /></div>
<div><input type="button" value="Add POI" onclick="addYahooPOI();"></div>
</div>
<div>
<select id="yahooPOISelect">
<option>Select a POI to Remove</option>
</select>
<input type="button" value="Remove POI" onclick="removeYahooPOI();"/>
</div>
<script type="text/javascript">
var yahooMap3;
var yahooPOIs = new Array();
function yahooInitializer3()
{
yahooMap3 = new YMap(document.getElementById('yahooMapDiv3'));
yahooMap3.addPanControl();
yahooMap3.addTypeControl();
yahooMap3.addZoomLong();
yahooMap3.addZoomScale();
yahooMap3.drawZoomAndCenter(new YGeoPoint(39.768519, -86.158041), 5);
}
scriptInitializers.push(yahooInitializer3);
function addYahooPOI()
{
var title = document.getElementById('yahooInfoTitle');
var lat = document.getElementById('yahooLat');
var lng = document.getElementById('yahooLng');
var newPOI = new YMarker(new YGeoPoint(parseFloat(lat.value), parseFloat(lng.value)));
newPOI.addLabel(title.value);
yahooMap3.addOverlay(newPOI);
var yahooPOISelect = document.getElementById('yahooPOISelect');
var newPOIOption = document.createElement('option');
newPOIOption.text = title.value;
var poiIndex = 0;
for ( poiIndex = 0; poiIndex < yahooPOIs.length; poiIndex++ )
{
if ( yahooPOIs[poiIndex] == null )
{
newPOIOption.value = poiIndex;
yahooPOISelect.add(newPOIOption);
yahooPOIs[poiIndex] = newPOI;
return;
}
}
newPOIOption.value = yahooPOIs.length;
yahooPOISelect.add(newPOIOption);
yahooPOIs.push(newPOI);
}
function removeYahooPOI()
{
var yahooPOISelect = document.getElementById('yahooPOISelect');
var poiIndex = parseInt(yahooPOISelect.value);
if (typeof poiIndex == 'number')
{
yahooMap3.removeOverlay(yahooPOIs[poiIndex]);
yahooPOIs[poiIndex] = null;
yahooPOISelect.remove(yahooPOISelect.selectedIndex);
}
}
</script>
<div>
<p>Adding a POI to a Yahoo map requires creating an instance of the YMarker class and then calling the YMap.addOverlay method.</p>
<div class="code-yahoo">
<ul>
<li>YMarker(YGeoPoint)
<li>YMap.addMarker(YGeoPoint)
<li>YMap.removeMarker(Id)
<li>YMap.addOverlay(YOverlay)
<li>YMap.removeOverlay(YOverlay)
<li>YMap.removeMarkersAll()
</ul>
</div>
<p>The YMarker class derives from the YOverlay class. The constructor takes a YGeoPoint referencing the location for the POI. The YMap also includes a the addMarker method for adding a POI using only the YGeoPoint. I recommend against using this form because you are not able to maintain a reference to the marker in this way without performing further queries on the map.</p>
<p>As we saw in the Bing and Google maps, the Yahoo maps API allows for setting a label on the marker. Unlike the Bing and Google maps, the label is always displayed. The label is applied by using the YMarker.addLabel method to apply the label the first time, and YMarker.reLabel to change the label.</p>
<p>Putting all of this together, here is the sample code used on this page for adding and removing POIs to the page.</p>
<div class="code-yahoo"><pre>
<div class="mapDiv" id="yahooMapDiv3"></div>
<div>
<div>Lattitude: <input id="yahooLat" type="text" value="39.768519" /></div>
<div>Longitude: <input id="yahooLng" type="text" value="-86.158041" /></div>
<div>Information Title: <input id="yahooInfoTitle" type="text" value="Title" /></div>
<div><input type="button" value="Add POI" onclick="addYahooPOI();"></div>
</div>
<div>
<select id="yahooPOISelect">
<option>Select a POI to Remove</option>
</select>
<input type="button" value="Remove POI" onclick="removeYahooPOI();"/>
<script type="text/javascript">
var yahooMap3;
var yahooPOIs = new Array();
function yahooInitializer3()
{
yahooMap3 = new YMap(document.getElementById('yahooMapDiv3'));
yahooMap3.addPanControl();
yahooMap3.addTypeControl();
yahooMap3.addZoomLong();
yahooMap3.addZoomScale();
yahooMap3.drawZoomAndCenter(new YGeoPoint(39.768519, -86.158041), 5);
}
scriptInitializers.push(yahooInitializer3);
function addYahooPOI()
{
var title = document.getElementById('yahooInfoTitle');
var lat = document.getElementById('yahooLat');
var lng = document.getElementById('yahooLng');
var newPOI = new YMarker(new YGeoPoint(parseFloat(lat.value), parseFloat(lng.value)));
newPOI.addLabel(title.value);
yahooMap3.addOverlay(newPOI);
var yahooPOISelect = document.getElementById('yahooPOISelect');
var newPOIOption = document.createElement('option');
newPOIOption.text = title.value;
var poiIndex = 0;
for ( poiIndex = 0; poiIndex < yahooPOIs.length; poiIndex++ )
{
if ( yahooPOIs[poiIndex] == null )
{
newPOIOption.value = poiIndex;
yahooPOISelect.add(newPOIOption);
yahooPOIs[poiIndex] = newPOI;
return;
}
}
newPOIOption.value = yahooPOIs.length;
yahooPOISelect.add(newPOIOption);
yahooPOIs.push(newPOI);
}
function removeYahooPOI()
{
var yahooPOISelect = document.getElementById('yahooPOISelect');
var poiIndex = parseInt(yahooPOISelect.value);
if (typeof poiIndex == 'number')
{
yahooMap3.removeOverlay(yahooPOIs[poiIndex]);
yahooPOIs[poiIndex] = null;
yahooPOISelect.remove(yahooPOISelect.selectedIndex);
}
}
</script>
</pre></div>
<p>We've seen from the first two posts that the Yahoo map API tends to be the most terse. This pattern continues with POI management, as there are no methods in the core API to assist with managing large sets of POIs, or visibility of POIs at various zoom levels. The YMarker class has hide and unhide methods for managing the visibility of the POI in your own code, but this will not be managed by the map.</p>
</div>
<h3>MapQuest</h3>
<div class="mapDiv" id="mapquestMapDiv3" style="width:640px;height:480px;"></div>
<div>
<div>Lattitude: <input id="mapquestLat" type="text" value="39.768519" /></div>
<div>Longitude: <input id="mapquestLng" type="text" value="-86.158041" /></div>
<div>Information Title: <input id="mapquestInfoTitle" type="text" value="Title" /></div>
<div><input type="button" value="Add POI" onclick="addMapquestPOI();"></div>
</div>
<div>
<select id="mapquestPOISelect">
<option>Select a POI to Remove</option>
</select>
<input type="button" value="Remove POI" onclick="removeMapquestPOI();"/>
</div>
<script type="text/javascript">
var mapquestMap3;
var mapquestPOIs = new Array();
function mapquestInitializer3()
{
mapquestMap3 = new MQA.TileMap(document.getElementById('mapquestMapDiv3'));
mapquestMap3.addControl(new MQA.LargeZoomControl());
mapquestMap3.addControl(new MQA.ViewControl());
mapquestMap3.setCenter(new MQA.LatLng(39.768519, -86.158041), 10);
}
scriptInitializers.push(mapquestInitializer3);
function addMapquestPOI()
{
var title = document.getElementById('mapquestInfoTitle');
var lat = document.getElementById('mapquestLat');
var lng = document.getElementById('mapquestLng');
var newPOI = new MQA.Poi(new MQA.LatLng(parseFloat(lat.value), parseFloat(lng.value)));
newPOI.infoTitleHTML = title.value;
mapquestMap3.addShape(newPOI);
var mapquestPOISelect = document.getElementById('mapquestPOISelect');
var newPOIOption = document.createElement('option');
newPOIOption.text = title.value;
var poiIndex = 0;
for ( poiIndex = 0; poiIndex < mapquestPOIs.length; poiIndex++ )
{
if ( mapquestPOIs[poiIndex] == null )
{
newPOIOption.value = poiIndex;
mapquestPOISelect.add(newPOIOption);
mapquestPOIs[poiIndex] = newPOI;
return;
}
}
newPOIOption.value = mapquestPOIs.length;
mapquestPOISelect.add(newPOIOption);
mapquestPOIs.push(newPOI);
}
function removeMapquestPOI()
{
var mapquestPOISelect = document.getElementById('mapquestPOISelect');
var poiIndex = parseInt(mapquestPOISelect.value);
if (typeof poiIndex == 'number')
{
mapquestMap3.removeShape(mapquestPOIs[poiIndex]);
mapquestPOIs[poiIndex] = null;
mapquestPOISelect.remove(mapquestPOISelect.selectedIndex);
}
}
</script>
<div>
<p>Adding a POI to a Mapquest map requires creating an instance of the MQA.Poi class and then calling the MQA.TileMap.addShape method.
<div class="code-mapquest">
<ul>
<li>MQA.Poi(MQA.LatLng)
<li>MQA.TileMap.addShape(VEShape)
<li>MQA.TileMap.removeShape(VEShape)
<li>MQA.TileMap.removeAllShapes()
</ul>
</div>
<p>The MQA.Poi constructor takes a MQA.LatLng referencing the location for the POI. There are two ways to remove the POI once it has been added to the map. If a reference is maintained to the POI, it may be later removed using the MQA.TileMap.removeShape method. Otherwise, all POIs may be removed at once by using the MQA.TileMap.removeAllShapes method.</p>
<p>As we saw in the Bing and Google maps, the MQA.Poi class may also show a label when the mouse is hovered over the POI by setting the MQA.Poi.infoTitleHTML property.</p>
<p>Putting all of this together, here is the sample code used on this page for adding and removing POIs to the page.</p>
<div class="code-mapquest"><pre>
<div class="mapDiv" id="mapquestMapDiv3" style="width:640px;height:480px;"></div>
<div>
<div>Lattitude: <input id="mapquestLat" type="text" value="39.768519" /></div>
<div>Longitude: <input id="mapquestLng" type="text" value="-86.158041" /></div>
<div>Information Title: <input id="mapquestInfoTitle" type="text" value="Title" /></div>
<div><input type="button" value="Add POI" onclick="addMapquestPOI();"></div>
</div>
<div>
<select id="mapquestPOISelect">
<option>Select a POI to Remove</option>
</select>
<input type="button" value="Remove POI" onclick="removeMapquestPOI();"/>
<script type="text/javascript">
var mapquestMap3;
var mapquestPOIs = new Array();
function mapquestInitializer3()
{
mapquestMap3 = new MQA.TileMap(document.getElementById('mapquestMapDiv3'));
mapquestMap3.addControl(new MQA.LargeZoomControl());
mapquestMap3.addControl(new MQA.ViewControl());
mapquestMap3.setCenter(new MQA.LatLng(39.768519, -86.158041), 10);
}
scriptInitializers.push(mapquestInitializer3);
function addMapquestPOI()
{
var title = document.getElementById('mapquestInfoTitle');
var lat = document.getElementById('mapquestLat');
var lng = document.getElementById('mapquestLng');
var newPOI = new MQA.Poi(new MQA.LatLng(parseFloat(lat.value), parseFloat(lng.value)));
newPOI.infoTitleHTML = title.value;
mapquestMap3.addShape(newPOI);
var mapquestPOISelect = document.getElementById('mapquestPOISelect');
var newPOIOption = document.createElement('option');
newPOIOption.text = title.value;
var poiIndex = 0;
for ( poiIndex = 0; poiIndex < mapquestPOIs.length; poiIndex++ )
{
if ( mapquestPOIs[poiIndex] == null )
{
newPOIOption.value = poiIndex;
mapquestPOISelect.add(newPOIOption);
mapquestPOIs[poiIndex] = newPOI;
return;
}
}
newPOIOption.value = mapquestPOIs.length;
mapquestPOISelect.add(newPOIOption);
mapquestPOIs.push(newPOI);
}
function removeMapquestPOI()
{
var mapquestPOISelect = document.getElementById('mapquestPOISelect');
var poiIndex = parseInt(mapquestPOISelect.value);
if (typeof poiIndex == 'number')
{
mapquestMap3.removeShape(mapquestPOIs[poiIndex]);
mapquestPOIs[poiIndex] = null;
mapquestPOISelect.remove(mapquestPOISelect.selectedIndex);
}
}
</script>
</pre></div>
<p>Of the examples shown so far, Bing had the most options available for dealing with scaling (visibility by zoom level, adding or removing a 'layer' of markers at once, and clustering management). Google had zoom level management, and Yahoo simply left it up to the coder. Mapquest meets or exceeds the functionality found in the Bing API. The MQA.Poi class has a minZoomLevel and maxZoomLevel property for restricting the marker to only be visible within certain zoom levels. In addition, the MQA.ShapeCollection can be used to show or hide an entire category of POIs at once.</p>
<div class="code-mapquest">
<ul>
<li>MQA.ShapeCollection()
<li>MQA.ShapeCollection.add()
<li>MQA.ShapeCollection.remove()
<li>MQA.TileMap.addShapeCollection(MQA.ShapeCollection)
<li>MQA.TileMap.removeShapeCollection(MQA.ShapeCollection)
<li>MQA.TileMap.replaceShapeCollection(MQA.ShapeCollection, MQA.ShapeCollection)
<li>MQA.TileMap.getShapeCollections()
</ul>
</div>
<p>Corresponding to the clustering feature found in Bing maps, Mapquest maps utilize a Declutter feature by default. The MQA.Declutter object can be retrieved from the map using the MQA.TileMap.getDeclutter method. The declutter mode can be set to one of the following using the MQA.Declutter.setDeclutterMode</p>
<div class="code-mapquest">
<ul>
<li>0 - No declutter
<li>1 - Stack decluttering
<li>2 - Leader-line decluttering
</ul>
</div>
</div>
<h3>Is That It?</h3>
<div><p>We actually skipped quite a bit here, such as creating custom icons for our POIs and using a larger information window. Creating custom icons is a fairly involved process fraught with gotchas, but I may try to touch on this in a future advanced topics post. The information window is not a difficult topic, but it is best addressed once we know how to react to a click event on the map. Before we get to handling events, the next topic to cover is geocoding and reverse-geocoding. There is a lot more to cover, and each week we get more value from our map. As always, if you are eager to learn more just follow these links to the vendor specific API documentation.</p>
<ul>
<li><a href="http://www.microsoft.com/maps/isdk/ajax/">Bing Maps Interactive SDK</a></li>
<li><a href="http://code.google.com/apis/maps/documentation/index.html">Google Maps API</a></li>
<li><a href="http://developer.mapquest.com/web/products/javascript/documentation">MapQuest JavaScript SDK Documentation</a></li>
<li><a href="http://developer.yahoo.com/maps/ajax/">Yahoo! Maps Web Services</a></li>
</ul>
</div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0tag:blogger.com,1999:blog-8743735145097954046.post-58983387815769853642010-03-02T08:40:00.007-05:002010-03-07T23:19:17.709-05:00Developing with Online Mapping APIs - Part 2: Adding Controls to a Map<div>
<p>We've cleared the first hurdle. We have a page that is displaying a map and the map is zoomed and centered on the location we want. This might be useful when the site user has no need to manipulate the map, but in many cases the user will want to pan the map around, change the zoom level, or even view a different type of map. To allow the user to do these things we need to add controls to our map.</p>
<p>All four mapping services support a basic set of controls that allow the user to do the following:</p>
<ul>
<li>Pan the map</li>
<li>Zoom the map (in or out)</li>
<li>Set the map type (road, satellite, topographical, etc.)</li>
</ul>
<p>Panning the map is the act of moving the current map view North, East, South, or West. Zooming controls change the area that is visible within the map, as well as the level of detail. Map types allow for viewing simple road maps, topographical maps, satellite views, oblique aerial views, and much more.</p>
<p>These controls are available in various sizes and styles to best suite your display, whether it is small and simple controls for a mobile device or large 3D controls for a desktop scenario. Let's take a look at the controls for our four service providers.</p>
</div>
<h3>Bing</h3>
<div class="mapDiv" id="bingMapDiv2"></div>
<script type="text/javascript">
var bingMap2;
function bingInitializer2()
{
bingMap2 = new VEMap('bingMapDiv2');
bingMap2.LoadMap(new VELatLong(39.768519, -86.158041), 13);
}
scriptInitializers.push(bingInitializer2);
function showBingScalebar(enabled)
{
if (enabled)
{
bingMap2.ShowScalebar();
}
else
{
bingMap2.HideScalebar();
}
}
function showBingMiniMap(enabled)
{
if (enabled)
{
bingMap2.ShowMiniMap();
}
else
{
bingMap2.HideMiniMap();
}
}
function setBingMapType()
{
var bingMapTypeSelect = document.getElementById('bingMapTypeSelect');
if ( bingMapTypeSelect.value == 'VEMapStyle.Road' )
{
bingMap2.SetMapStyle(VEMapStyle.Road);
}
if ( bingMapTypeSelect.value == 'VEMapStyle.Shaded' )
{
bingMap2.SetMapStyle(VEMapStyle.Shaded);
}
if ( bingMapTypeSelect.value == 'VEMapStyle.Aerial' )
{
bingMap2.SetMapStyle(VEMapStyle.Aerial);
}
if ( bingMapTypeSelect.value == 'VEMapStyle.Hybrid' )
{
bingMap2.SetMapStyle(VEMapStyle.Hybrid);
}
if ( bingMapTypeSelect.value == 'VEMapStyle.Oblique' )
{
bingMap2.SetMapStyle(VEMapStyle.Oblique);
}
if ( bingMapTypeSelect.value == 'VEMapStyle.Birdseye' )
{
bingMap2.SetMapStyle(VEMapStyle.Birdseye);
}
if ( bingMapTypeSelect.value == 'VEMapStyle.BirdseyeHybrid' )
{
bingMap2.SetMapStyle(VEMapStyle.BirdseyeHybrid);
}
}
function showBingDashboard(sizeName)
{
if (bingMap2 != null)
{
bingMap2.Dispose();
}
bingMap2 = new VEMap('bingMapDiv2');
if (sizeName == 'Normal')
{
bingMap2.SetDashboardSize(VEDashboardSize.Normal);
}
if (sizeName == 'Small')
{
bingMap2.SetDashboardSize(VEDashboardSize.Small);
}
if (sizeName == 'Tiny')
{
bingMap2.SetDashboardSize(VEDashboardSize.Tiny);
}
bingMap2.LoadMap(new VELatLong(39.768519, -86.158041), 13);
if (sizeName == 'None')
{
bingMap2.HideDashboard();
}
else
{
bingMap2.ShowDashboard();
}
}
</script>
<div>
Map Control Style: <input type="radio" name="bingMapControlStyle" value="Normal" checked="true" onclick="showBingDashboard(this.value);" />Normal
<input type="radio" name="bingMapControlStyle" value="Small" onclick="showBingDashboard(this.value);" />Small
<input type="radio" name="bingMapControlStyle" value="Tiny" onclick="showBingDashboard(this.value);" />Tiny
<input type="radio" name="bingMapControlStyle" value="None" onclick="showBingDashboard(this.value);" />None
</div>
<div>
Show Scale Legend <input type="checkbox" checked="true" onclick="showBingScalebar(this.checked);" />
</div>
<div>
Show Mini Map <input type="checkbox" onclick="showBingMiniMap(this.checked);" />
</div>
<div>
Show Map Type: <select id="bingMapTypeSelect" onchange="setBingMapType();">
<option value="VEMapStyle.Road">Road</option>
<option value="VEMapStyle.Shaded">Shaded</option>
<option value="VEMapStyle.Aerial">Aerial</option>
<option value="VEMapStyle.Hybrid">Hybrid</option>
<option value="VEMapStyle.Oblique">Oblique</option>
<option value="VEMapStyle.Birdseye">Birdseye</option>
<option value="VEMapStyle.BirdseyeHybrid">BirdseyeHybrid</option>
</select>
</div>
<div>
<p>By default, the Bing map will display a road map with controls that allow for panning and zooming, as well as selecting from the available map types (Road, Aerial, and Bird's Eye). The controls are called the Dashboard, and are displayed and hidden via the ShowDashboard and HideDashboard methods on the VEMap instance.</p>
<p>Bing offers five types of maps to display.</p>
<div class="code-bing">
<ul>
<li>VEMapStyle.Road // Road map
<li>VEMapStyle.Shaded // Road map with shaded contours
<li>VEMapStyle.Aerial // Satellite view
<li>VEMapStyle.Hybrid // Satellite view with road map overlay
<li>VEMapStyle.Oblique // Low altitude flight photo view
<li>VEMapStyle.Birdseye // Same as Oblique
<li>VEMapStyle.BirdseyeHybrid // An oblique view with a road map overlay
</ul>
</div>
<p>The map style is set via the VEMap.SetMapStyle method. In the sample map shown above, the onchange event of the select box calls a setMapType function, which sets the map type to display.</P>
<div class="code-bing">
<pre>function setMapType()
{
var bingMapTypeSelect = document.getElementById('bingMapTypeSelect');
if ( bingMapTypeSelect.value == 'VEMapStyle.Road' )
{
bingMap.SetMapStyle(VEMapStyle.Road);
}
if ( bingMapTypeSelect.value == 'VEMapStyle.Shaded' )
{
bingMap.SetMapStyle(VEMapStyle.Shaded);
}
if ( bingMapTypeSelect.value == 'VEMapStyle.Aerial' )
{
bingMap.SetMapStyle(VEMapStyle.Aerial);
}
if ( bingMapTypeSelect.value == 'VEMapStyle.Hybrid' )
{
bingMap.SetMapStyle(VEMapStyle.Hybrid);
}
if ( bingMapTypeSelect.value == 'VEMapStyle.Oblique' )
{
bingMap.SetMapStyle(VEMapStyle.Oblique);
}
if ( bingMapTypeSelect.value == 'VEMapStyle.Birdseye' )
{
bingMap.SetMapStyle(VEMapStyle.Birdseye);
}
if ( bingMapTypeSelect.value == 'VEMapStyle.BirdseyeHybrid' )
{
bingMap.SetMapStyle(VEMapStyle.BirdseyeHybrid);
}
}</pre>
</div>
<p>Bing offers three sizes of controls to display.</p>
<div class="code-bing">
<ul>
<li>VEDashboardSize.Normal
<li>VEDashboardSize.Small
<li>VEDashboardSize.Tiny
</ul>
</div>
<p>The normal control style is the default, and includes controls for changing the map type, panning the map, and zooming. This is the best control to display for regular desktop viewing. The small control style is better suited to mobile devices with less screen real estate. Here the panning controls are missing, displaying only a small set of zoom controls and map type selectors. The tiny control style is the most terse, with only the zoom controls available. This mode is best for very small map display, such or low resolution mobile devices. The control style must be set before the map is loaded. Any attempt to change the map style after it is loaded will be ignored. The map controls can be hidden and shown after the map is loaded. The scale legend can also be hidden or displayed after the map has been loaded. The important control methods to know are:</p>
<div class="code-bing">
<ul>
<li>VEMap.SetDashboardSize(VEDashboardSize); // Set the size of the controls (call before calling VEMap.LoadMap)
<li>VEMap.ShowDashboard(); // Show the map controls
<li>VEMap.HideDashboard(); // Hide the map controls
<li>VEMap.ShowScalebar(); // Show the scale legend
<li>VEMap.HideScalebar(); // Hide the scale legend
</ul>
</div>
<p>Bing also offers some features outside of the typical panning, scaling, and map type controls. One of these features is the inset map. The inset map displays the port of the map currently in the view and allows for easier panning. When the user pans within the inset map the current view is moved to where the inset map was panned to. The inset map may either display a road or aerial view, and can be positioned anywhere within the mapping frame.</p>
<div class="code-bing">
<ul>
<li>VEMap.ShowMiniMap(xoffset, yoffset, VEMiniMapSize)
<li>VEMiniMapSize.Large
<li>VEMiniMapSize.Small
</ul>
</div>
<p>The offset and size parameters are optional. If not specified, a small inset map is displayed just below and to the right of where the map controls are displayed. The unfortunate drawback here is that it can be very difficult to properly place the inset map (for instance, if you wanted to anchor it in the bottom right corner) as the pixel dimensions of the inset map are not specified. In addition, inset map can be positioned on top of the regular controls, making them impossible to use.</p>
<p>If your users will be using Internet Explore, the 3D map type is available. This map type utilizes a control that the user must install. This map style allows for 3D exploration of the map, including 3D building representations. In my opinion, the 3D map offers little practical use, and the requirement for installation of an embedded control coupled with the limitation to the Internet Explorer browser only makes this a feature I would not immediately target. I have not included examples on how to use the 3D map for this reason.</p>
</div>
<h3>Google</h3>
<div class="mapDiv" id="googleMapDiv2"></div>
<script type="text/javascript">
var googleMap2;
var googleCurrentMapControlStyle;
var googleCurrentZoomControlStyle;
var googleCurrentMapTypeControlStyle;
function googleInitializer2()
{
googleMap2 = new GMap2(document.getElementById("googleMapDiv2"));
googleMap2.setCenter(new GLatLng(39.768519, -86.158041), 13);
googleCurrentMapControlStyle = new GLargeMapControl3D();
googleMap2.addControl(googleCurrentMapControlStyle);
}
function setGoogleMapType()
{
var googleMapTypeSelect = document.getElementById('googleMapTypeSelect');
if ( googleMapTypeSelect.value == 'G_NORMAL_MAP' )
{
googleMap2.setMapType(G_NORMAL_MAP);
}
if ( googleMapTypeSelect.value == 'G_SATELLITE_MAP' )
{
googleMap2.setMapType(G_SATELLITE_MAP);
}
if ( googleMapTypeSelect.value == 'G_AERIAL_MAP' )
{
googleMap2.setMapType(G_AERIAL_MAP);
googleMap2.enableRotation();
}
if ( googleMapTypeSelect.value == 'G_HYBRID_MAP' )
{
googleMap2.setMapType(G_HYBRID_MAP);
}
if ( googleMapTypeSelect.value == 'G_AERIAL_HYBRID_MAP' )
{
googleMap2.setMapType(G_AERIAL_HYBRID_MAP);
googleMap2.enableRotation();
}
if ( googleMapTypeSelect.value == 'G_PHYSICAL_MAP' )
{
googleMap2.setMapType(G_PHYSICAL_MAP);
}
if ( googleMapTypeSelect.value == 'G_MAPMAKER_NORMAL_MAP' )
{
googleMap2.setMapType(G_MAPMAKER_NORMAL_MAP);
}
if ( googleMapTypeSelect.value == 'G_MAPMAKER_HYBRID_MAP' )
{
googleMap2.setMapType(G_MAPMAKER_HYBRID_MAP);
}
if ( googleMapTypeSelect.value == 'G_MOON_ELEVATION_MAP' )
{
googleMap2.setMapType(G_MOON_ELEVATION_MAP);
}
if ( googleMapTypeSelect.value == 'G_MOON_VISIBLE_MAP' )
{
googleMap2.setMapType(G_MOON_VISIBLE_MAP);
}
if ( googleMapTypeSelect.value == 'G_MARS_ELEVATION_MAP' )
{
googleMap2.setMapType(G_MARS_ELEVATION_MAP);
}
if ( googleMapTypeSelect.value == 'G_MARS_VISIBLE_MAP' )
{
googleMap2.setMapType(G_MARS_VISIBLE_MAP);
}
if ( googleMapTypeSelect.value == 'G_MARS_INFRARED_MAP' )
{
googleMap2.setMapType(G_MARS_INFRARED_MAP);
}
if ( googleMapTypeSelect.value == 'G_SKY_VISIBLE_MAP' )
{
googleMap2.setMapType(G_SKY_VISIBLE_MAP);
}
}
function setGoogleZoomControlStyle(zoomStyle)
{
if (googleCurrentZoomControlStyle != null)
{
googleMap2.removeControl(googleCurrentZoomControlStyle);
googleCurrentZoomControlStyle = null;
}
if ( zoomStyle == 'GScaleControl')
{
googleCurrentZoomControlStyle = new GScaleControl();
googleMap2.addControl(googleCurrentZoomControlStyle);
}
if ( zoomStyle == 'GSmallZoomControl3D')
{
googleCurrentZoomControlStyle = new GSmallZoomControl3D();
googleMap2.addControl(googleCurrentZoomControlStyle);
}
if ( zoomStyle == 'GSmallZoomControl')
{
googleCurrentZoomControlStyle = new GSmallZoomControl();
googleMap2.addControl(googleCurrentZoomControlStyle);
}
}
function setGoogleMapTypeControlStyle(mapTypeControlStyle)
{
if (googleCurrentMapTypeControlStyle != null)
{
googleMap2.removeControl(googleCurrentMapTypeControlStyle);
googleCurrentMapTypeControlStyle = null;
}
if (mapTypeControlStyle == 'GMapTypeControl')
{
googleCurrentMapTypeControlStyle = new GMapTypeControl();
googleMap2.addControl(googleCurrentMapTypeControlStyle);
}
if (mapTypeControlStyle == 'GMenuMapTypeControl')
{
googleCurrentMapTypeControlStyle = new GMenuMapTypeControl();
googleMap2.addControl(googleCurrentMapTypeControlStyle);
}
if (mapTypeControlStyle == 'GHierarchicalMapTypeControl')
{
googleCurrentMapTypeControlStyle = new GHierarchicalMapTypeControl();
googleMap2.addControl(googleCurrentMapTypeControlStyle);
}
}
function setGoogleMapControlStyle(mapControlStyle)
{
if (googleCurrentMapControlStyle != null)
{
googleMap2.removeControl(googleCurrentMapControlStyle);
googleCurrentMapControlStyle = null;
}
if (mapControlStyle == 'GLargeMapControl3D')
{
googleCurrentMapControlStyle = new GLargeMapControl3D();
googleMap2.addControl(googleCurrentMapControlStyle);
}
if (mapControlStyle == 'GLargeMapControl')
{
googleCurrentMapControlStyle = new GLargeMapControl();
googleMap2.addControl(googleCurrentMapControlStyle);
}
if (mapControlStyle == 'GSmallMapControl')
{
googleCurrentMapControlStyle = new GSmallMapControl();
googleMap2.addControl(googleCurrentMapControlStyle);
}
}
var googleScaleLegend;
function showGoogleScaleLegend(enabled)
{
if (enabled && googleScaleLegend == null )
{
googleScaleLegend = new GScaleControl();
googleMap2.addControl(googleScaleLegend);
}
else if ( !enabled && googleScaleLegend != null )
{
googleMap2.removeControl(googleScaleLegend);
googleScaleLegend = null;
}
}
var googleOverviewMap;
function showGoogleOverviewMap(enabled)
{
if (enabled && googleOverviewMap == null )
{
googleOverviewMap = new GOverviewMapControl();
googleMap2.addControl(googleOverviewMap);
}
else if ( !enabled && googleOverviewMap != null )
{
googleMap2.removeControl(googleOverviewMap);
googleOverviewMap = null;
}
}
var googleNavigationLabelControl;
function showGoogleNavigationLabelControl(enabled)
{
if (enabled && googleNavigationLabelControl == null )
{
googleNavigationLabelControl = new GNavLabelControl();
googleMap2.addControl(googleNavigationLabelControl);
}
else if ( !enabled && googleNavigationLabelControl != null )
{
googleMap2.removeControl(googleNavigationLabelControl);
googleNavigationLabelControl = null;
}
}
scriptInitializers.push(googleInitializer2);
</script>
<div>
Map Control Style: <input type="radio" name="googleMapControlStyle" value="GLargeMapControl3D" checked="true" onclick="setGoogleMapControlStyle(this.value);" />Large 3D
<input type="radio" name="googleMapControlStyle" value="GLargeMapControl" onclick="setGoogleMapControlStyle(this.value);" />Large
<input type="radio" name="googleMapControlStyle" value="GSmallMapControl" onclick="setGoogleMapControlStyle(this.value);" />Small
<input type="radio" name="googleMapControlStyle" value="None" onclick="setGoogleMapControlStyle(this.value);" />None
</div>
<div>
Zoom Control Style: <input type="radio" name="googleZoomControlStyle" value="GSmallZoomControl3D" onclick="setGoogleZoomControlStyle(this.value);" />Small 3D
<input type="radio" name="googleZoomControlStyle" value="GSmallZoomControl" onclick="setGoogleZoomControlStyle(this.value);" />Small
<input type="radio" name="googleZoomControlStyle" value="None" checked="true" onclick="setGoogleZoomControlStyle(this.value);" />None
</div>
<div>
Map Type Control Style: <input type="radio" name="googleMapTypeControlStyle" value="GMapTypeControl" onclick="setGoogleMapTypeControlStyle(this.value);" />Standard
<input type="radio" name="googleMapTypeControlStyle" value="GMenuMapTypeControl" onclick="setGoogleMapTypeControlStyle(this.value);" />Menu
<input type="radio" name="googleMapTypeControlStyle" value="GHierarchicalMapTypeControl" checked="true" onclick="setGoogleMapTypeControlStyle(this.value);" />Hierarchical
<input type="radio" name="googleMapTypeControlStyle" value="None" checked="true" onclick="setGoogleMapTypeControlStyle(this.value);" />None
</div>
<div>
Show scale legend <input type="checkbox" id="googleShowScaleLegendCheckBox" onclick="showGoogleScaleLegend(this.checked);" />
</div>
<div>
Show Overview Map <input type="checkbox" id="googleShowOverviewMap" onclick="showGoogleOverviewMap(this.checked);" />
</div>
<div>
Show Navigation Label Control <input type="checkbox" id="googleShowNavigationLabelControl" onclick="showGoogleNavigationLabelControl(this.checked);" />
</div>
<div>
Show Map Type: <select id="googleMapTypeSelect" onchange="setGoogleMapType();">
<option value="G_NORMAL_MAP">Road</option>
<option value="G_SATELLITE_MAP">Satellite</option>
<option value="G_AERIAL_MAP">Aerial</option>
<option value="G_HYBRID_MAP">Hybrid</option>
<option value="G_AERIAL_HYBRID_MAP">Aerial Hybrid</option>
<option value="G_PHYSICAL_MAP">Physical</option>
<option value="G_MAPMAKER_NORMAL_MAP">Mapmaker Normal</option>
<option value="G_MAPMAKER_HYBRID_MAP">Mapmaker Hybrid</option>
<option value="G_MOON_ELEVATION_MAP">Moon Elevation</option>
<option value="G_MOON_VISIBLE_MAP">Moon Visible</option>
<option value="G_MARS_ELEVATION_MAP">Mars Elevation</option>
<option value="G_MARS_VISIBLE_MAP">Mars Visible</option>
<option value="G_MARS_INFRARED_MAP">Mars Infrared</option>
<option value="G_SKY_VISIBLE_MAP">Sky</option>
</select>
</div>
<div>
<p>By default, the Google map does not display any controls. The user can still pan the map by clicking and dragging, and can zoom via the mouse wheel, but there are no dedicated controls on the screen. The Google Maps API provides a helper function on the GMap2 object to initialize the controls to the 'default' options that appear when using Google Maps on a desktop. These default controls include a panning control, zooming controls including a zoom scale bar, a map scale legend, and map type selector.</p>
<p>Google offers fifteen types of maps to display.</p>
<div class="code-google">
<ul>
<li>G_NORMAL_MAP // Road map
<li>G_SATELLITE_MAP // Satellite view
<li>G_AERIAL_MAP // Low altitude flight photo view
<li>G_HYBRID_MAP // Satellite view with road map overlay
<li>G_AERIAL_HYBRID_MAP // Low altitude flight photo view with road map overlay
<li>G_PHYSICAL_MAP // Topographical map
<li>G_MAPMAKER_NORMAL_MAP // Street map created using Google Mapmaker
<li>G_MAPMAKER_HYBRID_MAP // Transparent overlay of major streets created in Google Mapmaker
<li>G_MOON_ELEVATION_MAP // Terrain map of the lunar surface
<li>G_MOON_VISIBLE_MAP // Photographic map of the lunar surface
<li>G_MARS_ELEVATION_MAP // Terrain map of the martian surface
<li>G_MARS_VISIBLE_MAP // Photographic map of the martian surface
<li>G_MARS_INFRARED_MAP // Infrared map of the martian surface
<li>G_SKY_VISIBLE_MAP // Map of the sky, displaying the celestial sphere
<li>G_SATELLITE_3D_MAP // Utilizes Google Earth plug-in to display 3D model
</ul>
</div>
<p>While the moon, Mars, and Sky views are fun, for the purpose of this introduction I am going to concentrate on the typical road and satellite views. When the aerial view is unavailable the map will either display satellite imagery, or display empty tiles. In order to display aerial imagery the enableRotation method must be called on the map as well. The Mapmaker map types are composed of tiles submitted to Google Mapmaker from around the world. This map type can be useful when viewing areas of the world with poor map coverage through the standard mapping types. The map type can be set via code using the GMap2.setMapType method.</p>
<div class="code-google">
<pre>function setGoogleMapType()
{
var googleMapTypeSelect = document.getElementById('googleMapTypeSelect');
if ( googleMapTypeSelect.value == 'G_NORMAL_MAP' )
{
googleMap2.setMapType(G_NORMAL_MAP);
}
if ( googleMapTypeSelect.value == 'G_SATELLITE_MAP' )
{
googleMap2.setMapType(G_SATELLITE_MAP);
}
if ( googleMapTypeSelect.value == 'G_AERIAL_MAP' )
{
googleMap2.setMapType(G_AERIAL_MAP);
googleMap2.enableRotation();
}
if ( googleMapTypeSelect.value == 'G_HYBRID_MAP' )
{
googleMap2.setMapType(G_HYBRID_MAP);
}
if ( googleMapTypeSelect.value == 'G_AERIAL_HYBRID_MAP' )
{
googleMap2.setMapType(G_AERIAL_HYBRID_MAP);
googleMap2.enableRotation();
}
if ( googleMapTypeSelect.value == 'G_PHYSICAL_MAP' )
{
googleMap2.setMapType(G_PHYSICAL_MAP);
}
if ( googleMapTypeSelect.value == 'G_MAPMAKER_NORMAL_MAP' )
{
googleMap2.setMapType(G_MAPMAKER_NORMAL_MAP);
}
if ( googleMapTypeSelect.value == 'G_MAPMAKER_HYBRID_MAP' )
{
googleMap2.setMapType(G_MAPMAKER_HYBRID_MAP);
}
if ( googleMapTypeSelect.value == 'G_MOON_ELEVATION_MAP' )
{
googleMap2.setMapType(G_MOON_ELEVATION_MAP);
}
if ( googleMapTypeSelect.value == 'G_MOON_VISIBLE_MAP' )
{
googleMap2.setMapType(G_MOON_VISIBLE_MAP);
}
if ( googleMapTypeSelect.value == 'G_MARS_ELEVATION_MAP' )
{
googleMap2.setMapType(G_MARS_ELEVATION_MAP);
}
if ( googleMapTypeSelect.value == 'G_MARS_VISIBLE_MAP' )
{
googleMap2.setMapType(G_MARS_VISIBLE_MAP);
}
if ( googleMapTypeSelect.value == 'G_MARS_INFRARED_MAP' )
{
googleMap2.setMapType(G_MARS_INFRARED_MAP);
}
if ( googleMapTypeSelect.value == 'G_SKY_VISIBLE_MAP' )
{
googleMap2.setMapType(G_SKY_VISIBLE_MAP);
}
}</pre></div>
<p>Controls are added and removed from the Google map using GMap2.addControl(GControl) and GMap2.removeControl(GControl) respectively. The API supports a variety of pre-defined controls. There are three primary categories of controls: Panning and Zooming Controls, Zooming Only Controls, and Map Type Controls.</p>
<div class="code-google">
<ul>
<li>Panning and Zooming Controls
<ul>
<li>GLargeMapControl3D
<li>GLargeMapControl
<li>GSmallMapControl
</ul>
<li>Zooming Only Controls
<ul>
<li>GSmallZoomControl3D
<li>GSmallZoomControl
</ul>
<li>Map Type Controls
<ul>
<li>GMapTypeControl
<li>GMenuMapTypeControl
<li>GHierarchicalMapTypeControl
</ul>
</ul>
</div>
<p>The combined panning and zooming controls are useful for desktop browsing. The small version of the combined control is a good fit for small sidebar maps or mobile devices. The independent zoom control is useful for small screen resolutions and mobile devices as well. With both Bing and Google maps it is possible to create custom controls that facilitate any of these actions. However, it is useful to note that the Bing mapping API default controls do not allow for independent display of the map type control from the panning and zooming controls.</p>
<p>The Google map API also provides an inset map and a scale legend control. The inset map is called the Overview map. In addition, there is a control type called the Navigation Label Control. This is a "breadcrumbs" type control which will display a list of location names corresponding to zoom levels.</p>
<div class="code-google">
<ul>
<li>GOverviewMapControl
<li>GScaleControl
<li>GNavLabelControl
</ul>
</div>
<p>As with the Bing map, the Google map controls may be placed anywhere on the display aread by using the optional x and y offset parameters to the addControl method. By default, the panning and zooming controls appear in the upper left, the map type controls in the upper right, the scale legend in the lower left, and the inset map in the lower right. The navigation label control is also in the upper right by default and will compete with the map type controls for visibility.</p>
<p>Just as Bing maps offers a plug-in based view using the Bing Maps 3D plug-in, Google also permits the use of the Google Earth Plug-In. This map type utilizes the Google Earth Plug-In which the user must install. This map style allows for 3D exploration of the map, including 3D building representations. I will not be including any examples specific to the Google Earth Plug-In for the same reason that I do not reference the Bing Maps 3D plug-in.</p>
</div>
<h3>Yahoo</h3>
<div class="mapDiv" id="yahooMapDiv2"></div>
<script type="text/javascript">
var yahooMap2;
function yahooInitializer2()
{
yahooMap2 = new YMap(document.getElementById('yahooMapDiv2'));
yahooMap2.drawZoomAndCenter(new YGeoPoint(39.768519, -86.158041), 5);
}
scriptInitializers.push(yahooInitializer2);
function setYahooMapType(mapTypeName)
{
if (mapTypeName == 'YAHOO_MAP_REG')
{
yahooMap2.setMapType(YAHOO_MAP_REG);
}
if (mapTypeName == 'YAHOO_MAP_SAT')
{
yahooMap2.setMapType(YAHOO_MAP_SAT);
}
if (mapTypeName == 'YAHOO_MAP_HYB')
{
yahooMap2.setMapType(YAHOO_MAP_HYB);
}
}
function showYahooPanningControl(enabled)
{
if (enabled)
{
yahooMap2.addPanControl();
}
else
{
yahooMap2.removePanControl();
}
}
function showYahooMapTypeControl(enabled)
{
if (enabled)
{
yahooMap2.addTypeControl();
}
}
var displayingYahooZoomControl = false;
function setYahooZoomControlStyle(styleName)
{
if (displayingYahooZoomControl)
{
yahooMap2.removeZoomControl();
displayingYahooZoomControl = false;
}
if (styleName == 'Long')
{
yahooMap2.addZoomLong();
displayingYahooZoomControl = true;
}
if (styleName == 'Short')
{
yahooMap2.addZoomShort();
displayingYahooZoomControl = true;
}
}
function showYahooScaleLegend(enabled)
{
if (enabled)
{
yahooMap2.addZoomScale();
}
else
{
yahooMap2.removeZoomScale();
}
}
</script>
<div>
Show Panning Control <input type="checkbox" onclick="showYahooPanningControl(this.checked);" />
</div>
<div>
Show Map Type Control <input type="checkbox" onclick="showYahooMapTypeControl(this.checked);" />
</div>
<div>
Show Scale Legend <input type="checkbox" checked="true" onclick="showYahooScaleLegend(this.checked);" />
</div>
<div>
Zoom Control Style: <input type="radio" name="yahooZoomControlStyle" value="Long" onclick="setYahooZoomControlStyle(this.value);" /> Long
<input type="radio" name="yahooZoomControlStyle" value="Short" onclick="setYahooZoomControlStyle(this.value);" /> Short
<input type="radio" name="yahooZoomControlStyle" checked="true" value="None" onclick="setYahooZoomControlStyle(this.value);" /> None
</div>
<div>
Show Map Type: <select id="yahooMapTypeSelect" onchange="setYahooMapType(this.value);">
<option value="YAHOO_MAP_REG">Road</option>
<option value="YAHOO_MAP_SAT">Satellite</option>
<option value="YAHOO_MAP_HYB">Hybrid</option>
</select>
</div>
<div><p>By default, the Yahoo map does not display any controls. The user can still pan the map by clicking and dragging, and can zoom with double-click, but there are no controls on the screen to facilitate this. The only default control is the map scale legend.</p>
<p>Yahoo offers three types of maps to display.</p>
<div class="code-yahoo">
<ul>
<li>YAHOO_MAP_REG // Road map
<li>YAHOO_MAP_SAT // Satellite view
<li>YAHOO_MAP_HYB // Satellite view with road map overlay
</ul>
</div>
<p>The map type can be set via code using the YMap.setMapType method.</p>
<div class="code-yahoo">
<pre>function setYahooMapType(mapTypeName)
{
if (mapTypeName == 'YAHOO_MAP_REG')
{
yahooMap2.setMapType(YAHOO_MAP_REG);
}
if (mapTypeName == 'YAHOO_MAP_SAT')
{
yahooMap2.setMapType(YAHOO_MAP_SAT);
}
if (mapTypeName == 'YAHOO_MAP_HYB')
{
yahooMap2.setMapType(YAHOO_MAP_HYB);
}
}</pre></div>
<p>Controls are added and removed from the Yahoo map using helper functions on the YMap instance.</p>
<div class="code-yahoo">
<ul>
<li>Panning Controls
<ul>
<li>YMap.addPanControl()
<li>YMap.removePanControl()
</ul>
<li>Zooming Controls
<ul>
<li>YMap.addZoomLong()
<li>YMap.addZoomShort()
<li>YMap.removeZoomControl()
</ul>
<li>Map Type Controls
<ul>
<li>YMap.addTypeControl()
</ul>
</ul>
</div>
<p>Compared to the Bing and Google mapping API's, the methods for exposing controls on a Yahoo map are fairly terse. The position of the controls is not settable. Only the Zoom control has a style option, and this is to choose between a slider type zoom control and simple zoom in / zoom out controls. When switching between these styles you must first remove the existing style by calling the removeZoomControl method. The map type control can be added to the map, but is not removable.</p>
<p>Where both Bing and Google offer different sizes of controls, the Yahoo maps API has just one size. Fortunately the size of the controls is adequate to fit both a desktop and a mobile display.</p>
<p>The Yahoo map API also provides a scale legend control.</p>
<div class="code-yahoo">
<ul>
<li>YMap.addZoomScale()
<li>YMap.removeZoomScale()
</ul>
</div>
<p>The Yahoo map API does not include an inset map or 'breadcrump' navigation label feature. It also does not include any type of plug-in type display integration. These may sound like drawbacks, but they could also be considered a simplification of the API and display.</p>
</div>
<h3>MapQuest</h3>
<div class="mapDiv" id="mapquestMapDiv2" style="width:640px;height:480px;"></div>
<script type="text/javascript">
var mapquestMap2;
function mapquestInitializer2()
{
mapquestMap2 = new MQA.TileMap(document.getElementById('mapquestMapDiv2'));
mapquestMap2.setCenter(new MQA.LatLng(39.768519, -86.158041), 10);
}
function setMapquestMapType(mapType)
{
mapquestMap2.setMapType(mapType);
}
var mapquestPanningControl;
function showMapquestPanningControl(enabled)
{
if (enabled && mapquestPanningControl == null)
{
mapquestPanningControl = new MQA.PanControl();
mapquestMap2.addControl(mapquestPanningControl);
}
else if (!enabled && mapquestPanningControl != null)
{
mapquestMap2.removeControl(mapquestPanningControl);
mapquestPanningControl = null;
}
}
var mapquestMapTypeControl
function showMapquestMapTypeControl(enabled)
{
if (enabled && mapquestMapTypeControl == null)
{
mapquestMapTypeControl = new MQA.ViewControl();
mapquestMap2.addControl(mapquestMapTypeControl);
}
else if (!enabled && mapquestMapTypeControl != null)
{
mapquestMap2.removeControl(mapquestMapTypeControl);
mapquestMapTypeControl = null;
}
}
var mapquestTrafficControl
function showMapquestTrafficControl(enabled)
{
if (enabled && mapquestTrafficControl == null)
{
mapquestTrafficControl = new MQA.TrafficControl();
mapquestMap2.addControl(mapquestTrafficControl);
}
else if (!enabled && mapquestTrafficControl != null)
{
mapquestMap2.removeControl(mapquestTrafficControl);
mapquestTrafficControl = null;
}
}
var mapquestZoomControl
function setMapquestZoomControlStyle(style)
{
if (mapquestZoomControl != null)
{
mapquestMap2.removeControl(mapquestZoomControl);
mapquestZoomControl = null;
}
if (style == 'Large')
{
mapquestZoomControl = new MQA.LargeZoomControl();
mapquestMap2.addControl(mapquestZoomControl);
}
if (style == 'Small')
{
mapquestZoomControl = new MQA.ZoomControl();
mapquestMap2.addControl(mapquestZoomControl);
}
}
scriptInitializers.push(mapquestInitializer2);
</script>
<div>
Show Panning Control <input type="checkbox" onclick="showMapquestPanningControl(this.checked);" />
</div>
<div>
Show Map Type Control <input type="checkbox" onclick="showMapquestMapTypeControl(this.checked);" />
</div>
<div>
Show Traffic Control <input type="checkbox" onclick="showMapquestTrafficControl(this.checked);" />
</div>
<div>
Zoom Control Style <input type="radio" name="mapquestZoomControlStyle" value="Large" onclick="setMapquestZoomControlStyle(this.value);" />Large
<input type="radio" name="mapquestZoomControlStyle" value="Small" onclick="setMapquestZoomControlStyle(this.value);" />Small
<input type="radio" name="mapquestZoomControlStyle" value="None" checked="true" onclick="setMapquestZoomControlStyle(this.value);" />None
</div>
<div>
Show Map Type: <select id="mapquestMapTypeSelect" onchange="setMapquestMapType(this.value);">
<option value="map">Road</option>
<option value="sat">Satellite</option>
<option value="hyb">Hybrid</option>
</select>
</div>
<div><p>By default, the MapQuest map does not display any controls. The user can still pan the map by clicking and dragging, but there are no controls on the screen to facilitate this.</p>
<p>MapQuest offers three types of maps to display.</p>
<div class="code-mapquest">
<ul>
<li>map // Road map
<li>sat // Satellite view
<li>hyb // Satellite view with road map overlay
</ul>
</div>
<p>The map type can be set via code using the MQA.TileMap.setMapType method. Keep in mind that the map types listed above are strings, not constants.</p>
<div class="code-mapquest">
<pre>Show Map Type: <select id="mapquestMapTypeSelect" onchange="setMapquestMapType(this.value);">
<option value="map">Road</option>
<option value="sat">Satellite</option>
<option value="hyb">Hybrid</option>
</select>
<script>
function setMapquestMapType(mapType)
{
mapquestMap2.setMapType(mapType);
}
</script></pre></div>
<p>Controls are added and removed from the MapQuest map using the MQA.TileMap.addControl and MQA.TileMap.removeControl methods. The addControl method takes a reference to the control to add, as well as an optional MQA.MapCornerPlacement instance for custom placement on the map. One thing to remember is that the reference to the MapQuest JavaScript API in our header had some extra parameters that we did not see with Bing, Google, or Yahoo. One of those parameters was the ipkg parameter, which indicates which optional JavaScript packages are available. If you remember, we included the 'controls1' and 'traffic' packages in this parameter. The 'controls1' package is required for displaying any controls on the map, so if you find that you are getting strange results when attempting to add controls, make sure this package is included.</p>
<div class="code-mapquest">
<ul>
<li>Panning Controls
<ul>
<li>MQA.PanControl
</ul>
<li>Zooming Controls
<ul>
<li>MQA.ZoomControl
<li>MQA.LargeZoomControl
</ul>
<li>Map Type Controls
<ul>
<li>MQA.ViewControl
</ul>
</ul>
</div>
<p>The MapQuest controls are like the Yahoo controls in that there are only a few pre-defined control types, and they only come in one size. However, unlike the Yahoo controls, the MapQuest controls may be placed anywhere on the map. You may notice in playing with the map above that the LargeZoomControl includes a panning control. When using the LargeZoomControl it is not necessary to also add a PanControl.</p>
<p>The MapQuest map API will always display a scale legend. In addition, the MapQuest API also has an available Traffic control. Be sure that the JavaScript API reference includes 'traffic' in the list of included optional packages and this control will be available for display.</p>
<div class="code-mapquest">
<ul>
<li>MQA.TrafficControl
</ul>
</div>
<p>The traffic control allows the user to overlay a traffic congestion heat map onto the displayed map type.</p>
<p>The MapQuest map API does not include an inset map or 'breadcrumb' navigation label feature. It also does not include any type of plug-in type display integration. These may sound like drawbacks, but they could also be considered a simplification of the API and display.</p>
</div>
<h3>Is That It?</h3>
<div><p>In the first post of the series we conquered simply putting the map on the page. Now we've added some controls that allow our site user to move around the map, change the map type, change the zoom level, and other types of navigation. In the next post we'll take a look at putting points of interest on the map. Until then, if you would like to read more about each API you can check out the following resources.</p></div>
<ul>
<li><a href="http://www.microsoft.com/maps/isdk/ajax/">Bing Maps Interactive SDK</a></li>
<li><a href="http://code.google.com/apis/maps/documentation/index.html">Google Maps API</a></li>
<li><a href="http://developer.mapquest.com/web/products/javascript/documentation">MapQuest JavaScript SDK Documentation</a></li>
<li><a href="http://developer.yahoo.com/maps/ajax/#ex1">Yahoo! Maps Web Services</a></li>
</ul>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0tag:blogger.com,1999:blog-8743735145097954046.post-15930979390239497302010-02-26T09:07:00.023-05:002010-02-28T02:23:36.116-05:00Developing with Online Mapping APIs - Part 1: Displaying a Map<div class="post-content-normal">The very first objective is to be able to display a map on a web page. There are
just a few basic things we need in order to make this happen:</div>
<ul><li>A reference to the mapping API</li>
<li>An HTML element to display the map in</li>
<li>A bit of script to initialize the map</li></ul>
<div class="post-content-normal">To start, let's write a simple HTML page that will display our map.
</div>
<div class="code">
<HTML><br />
<HEAD><br />
</HEAD><br />
<BODY><br />
<DIV id="mapDiv" style="position:relative;width:640px;height:480px"></DIV><br />
</BODY><br />
</HTML>
</div>
<div class="post-content-normal"><p>Nothing particularly fancy here. We've created a page with a single DIV that will
be the display area for our map. There are no size restrictions on the map, but you'll want to pick something that
fits well with your page design.</p>
<p>Next we need to add a reference to the script that will allow us to create and interact with maps. We also need
a bit of custom script to create the map and set the initial display. The method for doing this is largely similar
across APIs, but we will look at them individually in the following sections to highlight the differences in each.</p>
</div>
<h3>Bing</h3>
<div class="mapDiv" id="bingMapDiv"></div>
<script type="text/javascript">
var bingMap;
function bingInitializer()
{
bingMap = new VEMap('bingMapDiv');
bingMap.LoadMap(new VELatLong(39.768519, -86.158041), 13);
}
scriptInitializers.push(bingInitializer);</script>
<div class="post-content-normal">To embed a Bing map on your page the first thing you will need to add is the
reference to the Bing map JavaScript API.</div>
<div class="code-bing"><script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script></div>
<div class="post-content-normal"><p>The only item of interest in the script reference is the v=6.2. This indicates the
version of the mapping API that your code would prefer to use. This can be useful if there is a API change resulting
in behavior that breaks your interface. Next, we need to add a bit of code to initialize our map. We will want to
create a global variable to hold a reference to our map object. For this example, we will initialize the map on the
page's onload event. There are two steps to initializing the map. The first step is to create the map object. The
second step is to load the map content centered on a location.</p></div>
<div class="code-bing">
<HTML><br />
<HEAD><br />
<script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script><br />
<SCRIPT><br />
var bingMap;<br />
function bingInitializer()<br />
{<br />
bingMap = new VEMap('mapDiv');<br />
bingMap.LoadMap(new VELatLong(39.768519, -86.158041), 13);<br />
}<br />
</SCRIPT><br />
</HEAD><br />
<BODY onload="bingInitializer()"><br />
<DIV id="mapDiv" style="position:relative;width:640px;height:480px"></DIV><br />
</BODY><br />
</HTML>
</div>
<div class="post-content-normal"><p>The constructor for the VEMap takes the HTML element that will host the map as an
argument. The LoadMap method requires a lattitude and longitude for centering the map, as well as a zoom level.
Unfortunately there is no standard for zoom levels across all APIs. In the case of Bing, the smaller the number the
farther out the camera view. The greater the number the closer in the view. When developing a page that will host
a Bing map, be absolutely certain that the 'position:relative' style is set on the HTML element the map will be
displayed within. If this style attribute is missing the Bing map will overflow beyond the bounds of the DIV in the
Google Chrome and Mozilla Firefox browsers.</p>
</div>
<h3>Google</h3>
<div class="mapDiv" id="googleMapDiv"></div>
<script type="text/javascript">
var googleMap;
function googleInitializer()
{
googleMap = new GMap2(document.getElementById("googleMapDiv"));
googleMap.setCenter(new GLatLng(39.768519, -86.158041), 13);
}
scriptInitializers.push(googleInitializer);
</script>
<div class="post-content-normal"><p>To embed a Google map on your page the first thing you will need to add is the
reference to the Google map JavaScript API.</p></div>
<div class="code-google"><script type="text/javascript" src="http://maps.google.com/maps?file=api&v=2&key=abcdefg&sensor=false"></script></div>
<div class="post-content-normal"><p>There are a few new items here. Again, we have a version refere (v=2) but now we
also see a key parameter. Your site must register with Google to obtain a key for using the API. Failing to do so
will result in a pop-up nag on your page. The advantage of registering for a key is that it eliminates the pop-up
nag, and if you decide to purchase the support, will allow access to additional calls and features. The sensor
parameter is used for enabling location aware services on mobile devices. We will leave this off for now, but I may
cover it a bit in a future post.</p>
<p>As with the Bing map, we need to write a bit of script to initialize our Google map.
We will create a global variable to hold reference to the map. Just like the Bing script, the Google script needs
to both create the map object and then load the map content centered on a location and zoom level.</p></div>
<div class="code-google">
<HTML><br />
<HEAD><br />
<SCRIPT type="text/javascript" src="http://maps.google.com/maps?file=api&v=2&key=abcdefg&sensor=false"></SCRIPT><br />
<SCRIPT><br />
var googleMap;<br />
function googleInitializer()<br />
{<br />
googleMap = new GMap2(document.getElementById("mapDiv"));<br />
googleMap.setCenter(new GLatLng(39.768519, -86.158041), 13); <br />
}<br />
</SCRIPT><br />
</HEAD><br />
<BODY onload="googleInitializer()"><br />
<DIV id="mapDiv" style="position:relative;width:640px;height:480px"></DIV><br />
</BODY><br />
</HTML>
</div>
<div class="post-content-normal"><p>Of note here is that, while the constructor for the Google Map object (GMap2) takes
the HTML element that will host the map as the argument, you must get a reference to the element rather than
supplying only the element ID. The zoom level value for Google Maps works just like Bing maps.</p></div>
<h3>Yahoo</h3>
<div class="mapDiv" id="yahooMapDiv"></div>
<script type="text/javascript">
var yahooMap;
function yahooInitializer()
{
yahooMap = new YMap(document.getElementById('yahooMapDiv'));
yahooMap.drawZoomAndCenter(new YGeoPoint(39.768519, -86.158041), 5);
}
scriptInitializers.push(yahooInitializer);
</script>
<div class="post-content-normal"><p>To embed a Yahoo map on your page the first thing you will need to add is the
reference to the Yahoo map JavaScript API.</p></div>
<div class="code-yahoo"><script type="text/javascript" src="http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=YD-eQRpTl0_JX2E95l_xAFs5UwZUlNQhhn7lj1H"></script></div>
<div class="post-content-normal"><p>Here again we see a version number (v=3.8) and a application ID.</p>
<p>No surprises when writing script to initialize the Yahoo map.</p></div>
<div class="code-yahoo">
<HTML><br />
<HEAD><br />
<SCRIPT type="text/javascript" src="http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=YD-eQRpTl0_JX2E95l_xAFs5UwZUlNQhhn7lj1H"></SCRIPT><br />
<SCRIPT><br />
var yahooMap;<br />
function yahooInitializer()<br />
{<br />
yahooMap = new YMap(document.getElementById('mapDiv'));<br />
yahooMap.drawZoomAndCenter(new YGeoPoint(39.768519, -86.158041), 5);<br />
}<br />
</SCRIPT><br />
</HEAD><br />
<BODY onload="yahooInitializer()"><br />
<DIV id="mapDiv" style="position:relative;width:640;height:480"></DIV><br />
</BODY><br />
</HTML>
</div>
<div class="post-content-normal"><p>The only difference to note between the Yahoo map and the other mapping APIs is
that the zoom value is reversed. Lower numbers mean closer to the ground while higher numbers mean farther
away. One interesting quirk: the yahoo scripts for displaying a map use a global JavaScript variable 'i'. This is a
common counting variable, and I'm guessing a bug on Yahoo's part. In any case, if you are using 'i' as a variable
in the same code that is loading a map, you are highly advised to rename the variable to something more descriptive
lest you find the Yahoo code has given it a new and unexpected value.</p></div>
<h3>MapQuest</h3>
<div class="mapDiv" id="mapquestMapDiv" style="width:640px;height:480px;"></div>
<script type="text/javascript">
var mapquestMap;
function mapquestInitializer()
{
mapquestMap = new MQA.TileMap(document.getElementById('mapquestMapDiv'));
mapquestMap.setCenter(new MQA.LatLng(39.768519, -86.158041), 10);
}
scriptInitializers.push(mapquestInitializer);
</script>
<div class="post-content-normal"><p>To embed a MapQuest map on your page the first thing you will need to add is the
reference to the MapQuest map JavaScript API.</p></div>
<div class="code-mapquest"><script type="text/javascript" src="http://btilelog.access.mapquest.com/tilelog/transaction?transaction=script&key=mjtd%7Clu6t2h07n1%2C2x%3Do5-lw7l9&itk=true&v=5.3.s&ipkg=controls1,traffic&ipr=false"></script></div>
<div class="post-content-normal"><p>Again we see a key parameter (key=) and a version number parameter (v=). There are
a few new players here though. The itk parameter is needed to include the Tile Map Toolkit in the scripts. Make sure
this is part of the include string, and always set to true. The ipkg paramter indicates which features of the map
are available to the user. Make sure this includes the controls1 value at a minimum. The ipr parameter is to
indicate if you are using the premium features of the API (true) or the free version (false).</p></div>
<div class="code-mapquest">
<HTML><br />
<HEAD><br />
<SCRIPT type="text/javascript" src="http://btilelog.access.mapquest.com/tilelog/transaction?transaction=script&key=mjtd%7Clu6t2h07n1%2C2x%3Do5-lw7l9&itk=true&v=5.3.s&ipkg=controls1,traffic&ipr=false"></SCRIPT><br />
<SCRIPT><br />
var mapquestMap;<br />
function mapquestInitializer()<br />
{<br />
mapquestMap = new MQA.TileMap(document.getElementById('mapDiv'));<br />
mapquestMap.setCenter(new MQA.LatLng(39.768519, -86.158041), 10);<br />
}<br />
</SCRIPT><br />
</HEAD><br />
<BODY onload="mapquestInitializer()"><br />
<DIV id="mapDiv" style="position:relative;width:640px;height:480px"></DIV><br />
</BODY><br />
</HTML>
</div>
<div class="post-content-normal"><p>The MapQuest map zoom values are like the Bing and Google maps (bigger number is
closer in) but the values do not correlate one to one like the Bing and Google maps do. A couple of notes are in
order with regard to the dimensions of the DIV used for displaying the MapQuest map. First, the MapQuest API
will not honor percentages when used for the map dimensions. If you try to create a map with an in-line style
of 100% for height and width you will find the map actually displayed at 100 pixels by 100 pixels. Second, the
MapQuest map API seems to have an issue with cascading style sheets. If the DIV that is hosting your map gets
the height and width set from CSS rather than an inline style attribute the API may apply the height dimension
to both height and width. This bug may be addressed in the future, but at present be sure to specify the
height and width values inline with the DIV tag.</p></div>
<h3>Is That It?</h3>
<div class="post-content-normal"><p>Getting the map on the page is just the start, and in the coming posts I will show
you how to do some more interesting things with the map, such as displaying a point of interest and getting a
lattitude and longitude value for an address. Until then, if you would like to read more about each API
you can check out the following resources.</p></div>
<ul>
<li><a href="http://www.microsoft.com/maps/isdk/ajax/">Bing Maps Interactive SDK</a></li>
<li><a href="http://code.google.com/apis/maps/documentation/index.html">Google Maps API</a></li>
<li><a href="http://developer.mapquest.com/web/products/javascript/documentation">MapQuest JavaScript SDK Documentation</a></li>
<li><a href="http://developer.yahoo.com/maps/ajax/#ex1">Yahoo! Maps Web Services</a></li>
</ul>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0tag:blogger.com,1999:blog-8743735145097954046.post-88409418055732963682010-02-26T00:13:00.003-05:002010-02-26T00:49:52.291-05:00Developing with Online Map APIsI'm planning to give a talk providing a basic introduction to the use of online mapping APIs. I plan to cover the big four: AOL (MapQuest), Google, Microsoft (Bing nee Virtual Earth), and Yahoo. The goal of the talk is to whet the appetite of site developers with the ease of putting a map on their site, and how valuable that map can be. I also hope to demonstrate where one mapping system provides particular advantages or disadvantages. <div>
</div><div>To kick things off, a bit of trivia: what is the most popular website for travel (and thus mapping)? If you said Google, you were right, but that has only been true for a few months. As recently as 2009 MapQuest had the crown as the number one portal for travel. HitWise has published <a href="http://www.hitwise.com/us/datacenter/main//dashboard-10133.html">their report for January 2010</a>, and there you can see that Google Maps has opened a commanding lead. Travel is a pretty broad topic though, and we're talking about mapping. I've chosen AOL, Google, Microsoft, and Yahoo for this discussion as they are the most popular services offering a public API to their mapping technology.</div><div>
</div><div>Over the course of the next few weeks I will provide a series of posts covering the following mapping tasks:</div><div><ul><li>Displaying a map on a page</li><li>Adding controls to the map</li><li>Adding points of interest to the map</li><li>Geocoding and Reverse Geocoding</li><li>Map events</li><li>Unique features to each API</li></ul><div>I hope that this series is informative and useful.</div></div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com1tag:blogger.com,1999:blog-8743735145097954046.post-72439838037505396892009-12-08T20:58:00.000-05:002010-02-22T09:22:56.972-05:00HP Mini 1000 - Upgrading the Transcend JetFlash T3A while back I posted about the <a href="http://onesadjam.blogspot.com/2009/05/hp-mini-1000-upgrades.html">HP Mini 1000 and some of the ways it can be upgraded</a>. Probably the easiest upgrade is to the Transcend JetFlash T3 drive that comes with the machine, and fills the recessed USB slot. The Mini comes with a 2GB drive, and the easiest upgrade is to simply purchase a higher capacity Transcend drive in the same line. The 4GB unit is priced at $16.70 on the <a href="http://ec.transcendusa.com/product/product_memory.asp?Cid=127&indexnum=3">Transcend store website</a>, or <a href="http://amzn.com/B001910GOO">$16.29 from Amazon</a>.<div><br /></div><div>I found a neat alternative. Kingston offers a MicroSDHC card that comes with a MicroSDHC card reader whose form factor is very similar to that of the JetFlash T3. The 4GB card with reader is priced at <a href="http://amzn.com/B0028QYC4O">$14.95 through Amazon</a> currently. I wanted to upgrade the MicroSD card in my GPSr anyway, so this seemed like a good opportunity to try it out. </div><div><br /></div><div>The first thing I noticed when I got the package was that the MicroSDHC reader is about 20% shorter than the Transcend card. Aside from that, the form factor was perfect. I slotted the 4GB MicroSDHC card into the reader, and then slide it into the small holder that covers the recessed USB port on the Mini. I slid everything into place, but nothing happened. Unfortunately, the shorter length means that the pads don't quite reach to make a connection to the USB port. All is not lost though. With a bit of fiddling, I found I could get the reader to slot into the recessed port without any issue. At that point, I can leave the cover on or off, it doesn't really matter, as the reader is completely within the recessed slot. If you do want to keep the cover on, be sure to slot the reader into the cover first, as trying to push the cover on over the reader after it has been slotted is a chore. Getting the reader back out again is a bit of a trick, but nothing you couldn't do with a handy set of tweezers, or a small set of needle nose pliers. </div><div><br /></div><div>Given the lower price of MicroSDHC card and reader at the same capacity, I think this is a pretty good way to upgrade your machine. Another advantage is that, as MicroSDHC card capacities increase and prices drop you can continue to upgrade this part. </div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0tag:blogger.com,1999:blog-8743735145097954046.post-10123445459535776182009-09-04T11:30:00.000-04:002010-02-22T09:22:56.976-05:00Rural InternetMy family and I are preparing to move out of the city and into a more rural setting. As part of that move, I've been researching what sorts of internet service will be available to us. At our current residence we have a number of different options for high speed internet service. We currently use the U-Verse service offered by AT&T. We are using the 1.5Mbps service, which adds $15 / month in cost to our U-Verse television bill. Comcast also offers high speed internet service, and I'm sure we could get DSL from any number of folks. Go a few miles outside of a dense residential area and your options quickly become limited.<div><br /></div><div>There are four types of service that I have found that will be available at our new location:</div><div><ul><li>Dial-Up</li><li>Satellite</li><li>Cellular Wireless</li><li>Microwave Wireless</li></ul><div><b><span class="Apple-style-span" style="font-size: large;"><span class="Apple-style-span" style="text-decoration: underline;">Dial-Up</span></span></b></div><div><span class="Apple-style-span" style="font-size: medium;">Dial-up internet service is the old standby that hasn't changed in over 10 years. The top speed is still pegged at 56kbps (ignoring the "speed boosting" tech that some vendors claim). Depending on the service provider, rates run anywhere from $10-$20 / month. However, we don't plan on having a home phone, so add to this cost the price to install a home phone (around $25 / month with AT&T) for the sole purpose of using dial-up, and it comes to $35 or more per month for that sluggish dial-up internet connection. It would be useful for the most basic internet uses: browsing basic web pages and sending e-mail. Forget about online gaming or rich web media though. Compare this to our U-Verse service, which is the equivalent of 1,500kbps for $20 less each month, and it would be a serious step backward.</span></div><div><span class="Apple-style-span" style="font-size: medium;"><br /></span></div><div><b><span class="Apple-style-span" style="text-decoration: underline;"><span class="Apple-style-span" style="font-size: large;">Satellite</span></span></b></div><div><span class="Apple-style-span" style="font-size: medium;">There are a few satellite internet providers, Hughes and WildBlue being the most prominent. Hughes offers 1.0Mbps down, 128kbps up service for $60 / month, while WildBlue tiers their service at 512K down / 128K up for $50, or 1.5M down / 256K up for $80. Those are fine speeds, if a little pricey. The real gotcha here is latency. It used to be that with satellite internet you only received data over the dish, and all of your uploads were on your telephone line. Now you get both your up and down data from the dish, but the latency can be anywhere up to 5 seconds. Compare that to the typical sub 0.1 second latency of other internet connections, and it is a big downer. This makes the satellite internet service unusable for things like voice chat, VPN connection for working at home, or online gaming. Using a VPN connection and online gaming are high priorities for me from my internet connection, so that eliminated satellite from contention.</span></div><div><span class="Apple-style-span" style="font-size: medium;"><br /></span></div><div><b><span class="Apple-style-span" style="text-decoration: underline;"><span class="Apple-style-span" style="font-size: large;">Cellular Wireless</span></span></b></div><div><span class="Apple-style-span" style="font-size: medium;">Cell phone companies offer data plans for their users who have smart phones (Blackberry, iPhone, Android, etc.). This offers a fairly speedy (348kbps or faster) way to access the internet. Most carriers offer mobile broadband service with the intention that you use it occasionally with your laptop, not as your dedicated home connection. Across providers, the standard seems to be to offer up to 5GB of downloads per month for $60 / month. That may seem like a lot, but it really isn't when it is your dedicated connection for home. You can quickly exhaust that 5GB quota and start paying exorbitant rates per additional kilobyte downloaded. For example, let's say a new product revision is released, and the download is 1GB or more. If there are alternate versions, I could exhaust all 5GB in a single afternoon. The idea of these caps is to prevent folks from hogging the network with P2P applications, swapping movies all day, and to keep usage as intended: occasional use on a mobile device. The net effect for me is that cellular is not an option as a home ISP.</span></div><div><span class="Apple-style-span" style="font-size: medium;"><br /></span></div><div><b><span class="Apple-style-span" style="text-decoration: underline;"><span class="Apple-style-span" style="font-size: large;">Microwave Wireless</span></span></b></div><div><span class="Apple-style-span" style="font-size: medium;">I found microwave wireless service to be the best mix of speeds, price, and availability. There are several service providers that can provide service to our location. With microwave wireless you need line-of-sight to the tower providing the signal. A small antenna is mounted on your home and communicates wirelessly with the main tower. Depending on geography and tree line, these systems can offer service in a 15-30 mile radius around a tower. Prices vary by speed, with it ranging from $35 / month for 512K down, 256 up service to $90 / month for speeds over 1Mbps. There isn't a single dominant player in this market like there is in the national cellular market. The best deals I found were offered by a local company: Hoosier Broadband.</span></div><div><span class="Apple-style-span" style="font-family:monospace, serif;font-size:100%;"><span class="Apple-style-span" style="font-size: 13px; white-space: pre-wrap;"><br /></span></span></div><div><span class="Apple-style-span" style=""><span class="Apple-style-span" style="font-family: monospace; font-size: 13px; white-space: pre-wrap; ">512K/256 $34.95 Residential Service</span></span></div><div><span class="Apple-style-span" style=""><span class="Apple-style-span" style="font-family: monospace; font-size: 13px; white-space: pre-wrap; ">768K/384 $44.95 Residential Service</span></span></div><div><span class="Apple-style-span" style=""><span class="Apple-style-span" style="font-family: monospace; font-size: 13px; white-space: pre-wrap; ">1.5M/512 $69.95 Residential Service</span></span></div><div><span class="Apple-style-span" style=""><span class="Apple-style-span" style="font-family: monospace; font-size: 13px; white-space: pre-wrap; ">512K/256 $54.95 Business Service</span></span></div><div><span class="Apple-style-span" style=""><span class="Apple-style-span" style="font-family: monospace; font-size: 13px; white-space: pre-wrap; ">768K/384 $64.95 Business Service </span></span></div><div><span class="Apple-style-span" style="font-family:monospace, serif;font-size:100%;"><span class="Apple-style-span" style="font-size: 13px; white-space: pre-wrap;"><br /></span></span></div><div><span class="Apple-style-span" style="font-size: medium;">The difference between residential service and business service is that Hoosier Broadband reserves the right to lower the priority of residential traffic in deference to their business customers, and business customers are guaranteed support within 24 hours. Residential customers are not. I'm not yet decided on whether I would choose the business or residential service. As the majority of my usage is late at night, I think the residential service should suffice, but given that I also intend to use this service when I need to work from home, the business service might be more prudent. Either way, I would be selecting the 768K service, which is half of what we get now from AT&T. A cut back, yes, but not a terrible one. </span></div><div><span class="Apple-style-span" style="font-size: medium;"><br /></span></div><div><span class="Apple-style-span" style="font-weight: bold; "><span class="Apple-style-span" style="text-decoration: underline;"><span class="Apple-style-span" style="font-size: large;">DSL, FiOS, and ISDN</span></span></span></div><div><span class="Apple-style-span" style="font-size: medium;">Just a quick note on these services: they aren't available. In fact, you probably can't remember the last time (or ever) hearing about ISDN. When I contacted the phone company, they said they are not selling new connections, but only maintaining existing accounts. That's fine, as ISDN has all of the drawbacks of dial-up with a higher cost and only barely better speeds. FiOS would be awesome, but there's no chance of getting that in a rural area as it is too expensive for Verizon to pull new fiber down country roads for a handful of customers. Maybe someday DSL will be an option, but not now.</span></div><div><span class="Apple-style-span" style="font-size: medium;"><br /></span></div><div><b><span class="Apple-style-span" style="text-decoration: underline;"><span class="Apple-style-span" style="font-size: large;">Summary</span></span></b></div><div><span class="Apple-style-span" style="font-size: medium; "><b style=""><span class="Apple-style-span" style="font-weight: normal; -webkit-text-decorations-in-effect: none; font-size: 16px; "><div><span class="Apple-style-span" style="font-size: medium; ">I think that microwave wireless is one service that we will hear a lot about in the coming years. One of the president's major policy initiatives is to increase access to broadband internet service for rural Americans. Installing a tower for wireless transmission is one of the most cost effective ways to do that, and the FCC is in active talks concerning opening up more of our wireless spectrum for data traffic (this is a major reason why we had the recent switch from analog to digital television over the air). I'm hopeful that these changes will result in more options for me as rural internet consumer, and lower prices.</span></div><div style="text-decoration: underline;"><span class="Apple-style-span" style="font-size: medium; "><br /></span></div></span></b></span></div></div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com1tag:blogger.com,1999:blog-8743735145097954046.post-20135025211003979772009-07-21T10:02:00.000-04:002010-02-22T09:22:56.983-05:00HP Mini 1000 Internal Microphone on WIndows 7One of the only complaints I had about my HP Mini 1000 netbook when I purchased it was the lack of a dedicated microphone jack. The Mini has a combined headphone/microphone jack. You can plug in a set of headphones and get sound, or plug in a mic and record, but not both at once. I like to use Skype, ooVoo, Ventrillo, and other voice applications, and this seemed like an unnecessary limitation. <div><br /></div><div>After loading up Windows 7 on my netbook, I was further disappointed to find that the default driver did not detect if you plugged in a mic or headphone, it just always worked as a headphone jack. This left me with no option for recording other than to use a plantronics USB headset that I had laying around. Taking up one of two USB ports for a headset was less than ideal.</div><div><br /></div><div>Then I read on a forum that the Mini has an internal microphone. What's this? Hidden, undocumented equipment? Sure enough, half-way between the left edge of the screen and the webcam there is a pinhole microphone. Unfortunately, I couldn't find a way to enable. I did see it was listed in my recording devices, but I couldn't get it to pick up sound. </div><div><br /></div><div>After a bit of searching, I found a link to a driver for the sound chip in the Mini 1000:</div><div><br /></div><div><a href="ftp://ftp.hp.com/pub/softpaq/sp40501-41000/sp40662.exe">ftp://ftp.hp.com/pub/softpaq/sp40501-41000/sp40662.exe</a></div><div><br /></div><div>I believe this is the official vista driver for the sound chip. After downloading and installing I went to the recording devices menu and sure enough, it is picking up sound! Yeah! The only quirk is that it appears the labels for the recording devices are switched. The recording device marked as External Mic is picking up sound, while the recording device marked Integrated Microphone Array is not. I just switched the default device and now I can carry on Skype video calls without any problems.</div><div><br /></div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com2tag:blogger.com,1999:blog-8743735145097954046.post-62739211521092229892009-05-05T22:59:00.000-04:002010-02-22T09:22:56.987-05:00HP Mini 1000 UpgradesOwn an HP Mini 1000, or looking to buy one, and want to know what parts can be upgraded? I've been poking around to see what is available for this great little netbook, and here are some of the things I've found.<div><br /></div><div><span class="Apple-style-span" style="font-size: large;"><span class="Apple-style-span" style="font-weight: bold;">Memory</span></span></div><div>The Mini comes with either 512MB or 1GB of memory. This is definitely the easiest part to upgrade. The documentation seems to suggest that 1GB is the max for this device, but mine took a 2GB Kingston DDR2 chip without any complaints.</div><div><br /></div><div><span class="Apple-style-span" style="font-size: large;"><span class="Apple-style-span" style="font-weight: bold;">Hard Drive</span></span></div><div><span class="Apple-style-span" style="font-size: medium;">The hard drive is a bit trickier. You'll need to take the keyboard off to get access to it. If you have a unit with a SSD, you'll also need to disconnect the recessed third USB port. There is a good demonstration of how to do all of this here: <a href="http://jkkmobile.blogspot.com/2009/01/runcore-18-inch-ssd-on-hp-mini-1000.html">http://jkkmobile.blogspot.com/2009/01/runcore-18-inch-ssd-on-hp-mini-1000.html</a></span></div><div><br /></div><div><span class="Apple-style-span" style="font-size: large;"><span class="Apple-style-span" style="font-weight: bold;">HP Mini Drive</span></span></div><div>If you purchased with the SSD option, you probably received a 2GB "HP Mini Drive". This is actually just a <a href="http://www.transcendusa.com/Products/ModDetail.asp?ModNo=181&LangNo=0&Func1No=&Func2No=">Transcend JetFlash T3</a> with a small plastic piece to help it blend nicely with the body of the netbook. You could either order a larger unit from HP, or you could get an original Transcend unit. As of this writing HP is selling the 4GB for $24.99, while transcend has the same unit priced at $17.60. Transcend also offers an 8GB drive for $32.30, which HP does not currently offer.</div><iframe allowfullscreen='allowfullscreen' webkitallowfullscreen='webkitallowfullscreen' mozallowfullscreen='mozallowfullscreen' width='320' height='266' src='https://www.blogger.com/video.g?token=AD6v5dwvf-jb3Vbi4Ea6tkaE1wAu4GNkyrkTUEtIqSOfXVCODQiTVkjcb7EHiyzHmeyIwIOBqpao9RpApgOW918zEg' class='b-hbp-video b-uploaded' frameborder='0'></iframe><div><br /></div><div><span class="Apple-style-span" style="font-size: large;"><span class="Apple-style-span" style="font-weight: bold;">WWAN</span></span></div><div>There has been a lot of chatter lately about cellular carriers offering discounted netbooks on a 2-year data contract. The HP Mini 1000 is poised to be a part of that offering. If you ordered yours with the WWAN card built in, you are already set. If not, you can still add one later. This video from <a href="http://jkkmobile.blogspot.com/2008/12/how-to-3g-hsdpa-to-hp-mini-1000.html">jkkmobile demonstrates how to add a WWAN module</a> to your netbook. Again, this involves taking off the keyboard and doing some pretty serious tinkering, so it isn't recommended for the faint of heart.</div><div><br /></div><div><span class="Apple-style-span" style="font-size: large;"><span class="Apple-style-span" style="font-weight: bold;">Battery</span></span></div><div>The stock 3-cell battery will net you anywhere from 1 to 3 of continuous use, depending on your power settings and what you are doing. HP now offers a 6-cell replacement battery, <a href="http://www.shopping.hp.com/product/computer/categories/notebook_batteries/1/accessories/FZ332AA%2523ABB">available for order here</a>.</div><div><br /></div><div><span class="Apple-style-span" style="font-size: large;"><span class="Apple-style-span" style="font-weight: bold;">External VGA</span></span></div><div>This one is a stickler. That funky connector on the left of the unit is where you plug in a propietary VGA adapter cable. Unfortunately, these are both in short supply and full of issues. Users who have the cable are reporting that it refuses to output anything but 1024x768 regardless of what display is attached. This is bad news for anyone hoping to connect to a projector.</div><div><br /></div><div>I love my netbook and I am getting a lot of great use from it. It is running Windows 7 RC1 like a champ, and I'm even able to do a little development in Visual Studio on it. So far I have only upgraded the memory, but I'm tempted to upgrade the Transcend flash drive. I'm holding off on a SSD upgrade until prices come down, but it is tempting as I currently only have about 1.5GB of free space on the main drive. Even with a relatively "stock" configuration, I find this unit to be a very nice, performant, and portable computing platform.</div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0tag:blogger.com,1999:blog-8743735145097954046.post-30558055851827169582009-04-24T12:43:00.000-04:002010-02-22T09:22:56.997-05:00Netbook, Windows 7, and Ubuntu Netbook RemixA couple of weeks ago I picked up a HP Mini 1000 netbook. I had a couple of intentions for this netbook. Firstly, I wanted to use it for any personal network use wherever I went. Rather than using my work PC for checking account balances, personal e-mail, and other non-work related network uses, I would use the netbook instead. Second, I wanted a portable machine that I could do some light development on. Nothing complicated mind you, but just the ability to open up a small project, hack some code, and check that it compiles.<div><br /></div><div>After playing around with the netbook for a while, I can safely say that I am very happy with the decision to purchase it. Netbooks are the latest consumer trend in computing, and there are a lot of models out there. You'll find that they are all very, very similar. The typical unit has two or three USB ports, a 1.6GHz Intel Atom processor, 512MB or 1GB or RAM, and a small solid state hard drive, or possibly a traditional magnetic drive. Most come bundled with a simplified Linux operating system, or Windows XP for a slight price premium. With so many vendors and so little to differentiate them it can be difficult to settle on one. I chose the HP model mostly because the keyboard felt the most comfortable to type on. Otherwise, there isn't much to differentiate it from the other $350 10" netbooks on the market.</div><div><br /></div><div>I made some modifications to my netbook before even powering it on. First, I upgraded the RAM to 2GB, which only cost $20. Next, rather than use the provided Windows XP operating system I decided to load the beta of Windows 7 (with some help from my colleague Mike Hall who had done the exact same thing to his own netbook). I loaded Visual Studio 2008, Google Chrome, and a handful of utilities that I like. After install my 16GB SSD has about 1.5GB of remaining free space. I put all of my code on the provided 2GB "HP Mini Drive", and I still have two USB ports and an SD Card slot to expand memory further, if necessary.</div><div><br /></div><div>I'm very happy with Windows 7. Even on the relatively weak processing power of the netbook it runs like a champ. I definitely like it as a replacement for Windows XP. Visual Studio runs well, if a bit sluggishly, and I am able to compile projects as I had hoped.</div><div><br /></div><div>Still, this beta is eventually going to run out, and at that time I need to make a decision between paying for a license for Windows 7 (if it is available) or selecting some other operating system. I could always revert to Windows XP, which came with the unit, at no cost. Or I could try one of the linux variants on the market, also at no or little cost. A friend of mine tipped me off to the <a href="http://www.ubuntu.com/getubuntu/download-netbook">Netbook Remix of Ubuntu</a>. Version 9.04 of the Linux OS just released, so I decided to download it and give it a try. One of the nice features of this release is that you can try it by loading it on a USB stick as a "live" OS, which means you can run it without wiping out the OS already on the machine.</div><div><br /></div><div>After playing with the netbook remix for a bit, I was impressed. I'm not ready to swap out my Win7 install, but when the beta expires it will definitely be something I consider. With MonoDevelop 2.0 and the Mono framework running I could probably still do some light C# work. One feature of the OS that I really liked was how UI works to maximize screen real estate for the foreground application. One of my frustrations with running Chrome (or any other browser) on Windows 7 is how much of my precious screen is taken up by title bars, menu bars, toolbars, bookmark bars, status bars, and task bars. With limited vertical space, it leaves only just enough room for my active application. This is exacerbated by sites like Google Reader that have a static header that cannot be minimized.</div><div><br /></div><div>In summary, I'm very happy with the utility I am getting out of my netbook, and I think Windows 7 is a solid OS choice for the platform. I'm impressed by what I see in Ubuntu Netbook Remix as well, and I may give it a try when my beta license expires.</div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0tag:blogger.com,1999:blog-8743735145097954046.post-68180930544144250972009-03-29T22:59:00.000-04:002010-02-22T09:22:57.003-05:00RAID<div> I've been considering some sort of network storage solution for some time now. We have three PC's in the house and our family pictures, videos, documents, and music are spread among them. Worse, we have only taken one backup of the pictures, and the rest are squirreled away in unknown folder levels of each of the machines. I wanted to change that. I had a couple of options in mind: a Network Attached Storage (NAS) device, a Windows Home Server (WHS), or setting up a share on my main PC. I came into a little luck and found myself with four 320GB hard drives, which significantly helped matters.<div><br /></div><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiP9jnECuOZMfkZ8ba8-5RRDCts0MxEZwyoZxx0ytslEFIc36rv1Jc8FDsFxJavbVtiOlFfyVyRBLjLFRZ7Gnm_IwfE0iTWJhBcOb6PzGAuS_Bdq9yErBpPvItiOF5SZ0kFf1CWaYkqQJEG/s1600-h/drobo-2lg.jpg"><img style="cursor:pointer; cursor:hand;width: 320px; height: 275px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiP9jnECuOZMfkZ8ba8-5RRDCts0MxEZwyoZxx0ytslEFIc36rv1Jc8FDsFxJavbVtiOlFfyVyRBLjLFRZ7Gnm_IwfE0iTWJhBcOb6PzGAuS_Bdq9yErBpPvItiOF5SZ0kFf1CWaYkqQJEG/s320/drobo-2lg.jpg" border="0" alt="" align="left" id="BLOGGER_PHOTO_ID_5318811275814694066" /></a>One option I was considering is the Drobo, which is a USB / Firewire storage device from Data Robotics. The Drobo is a box that can attach to your PC via USB or Firewire, and has slots for up to four hard drives. You can add hard drives in any pattern that you like, and as the system gets full you are prompted to add more storage. It uses a proprietary RAID format, offering some data security (if a drive fails, you won't lose everything). As far as simple solutions, this is tops. Just plug it in and insert your drives. The first generation unit is $350 and the current generation is $450. There is also an add-on piece for $200 that allows you to connect the Drobo directly to the network without any need for a PC to host it. In that form, it can truly be called a NAS.</div><div>I also investigated other NAS products, including those from ASUS, IOGear, and Western Digital. Those targeted to the home and small office offered two drive slots and a reasonable price, while those targeted at businesses offered four or more slots but at a premium price.</div><div><br /></div><div><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhCggocFJ9DPONDJSOM1qVuyzWkVUOksQ05y_10rKLvDjTmN-0896orvJVLKMOATSKEplhiOUwAzDo3P0haKEB-eE5gn_AtLRivJzkt0rUNeb0X5m25ok5lbbB9XrEthE3EqjOUdxzF2Dp_/s1600-h/windows-home-server.jpg"><img style="float:right; margin:0 0 10px 10px;cursor:pointer; cursor:hand;width: 257px; height: 256px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhCggocFJ9DPONDJSOM1qVuyzWkVUOksQ05y_10rKLvDjTmN-0896orvJVLKMOATSKEplhiOUwAzDo3P0haKEB-eE5gn_AtLRivJzkt0rUNeb0X5m25ok5lbbB9XrEthE3EqjOUdxzF2Dp_/s320/windows-home-server.jpg" border="0" alt="" id="BLOGGER_PHOTO_ID_5318816385026888658" /></a>Windows Home Server is an interesting product that also had my eye. I've known a few people to have them and all have had positive experiences. One of the biggest benefits of a WHS over a simple NAS is that ability to take complete backups of all of the PC's on your network in an automated fashion. Once scheduled, you can competely forget about this operation that will take periodic snapshots of your PC's. If one of those machines should fail, just replace the busted drive and restore it from WHS. The hardware for a WHS machine is fairly cheap (can be built for around $300). Unfortunately, at that price point you are still looking at just a two drive setup.</div><div><br /></div><div>In the end, I decided to setup a RAID on my existing PC and share the drive to the network. I chose to use a RAID-5 configuration. There are several flavors of RAID, and each has it's own nuances. There is RAID-0, which alternates the usage of two drives in "stripes". The advantage here is that rather than pulling your files from just one drive, it uses both drives equally, improving performance. It provides no data protection though, so if one of those drives fails, you are toast. Next is RAID-1, also called mirroring. In RAID-1, two drives are used and are perfect copies of each other. As with RAID-0, both drives can be used when reading files, leading to better read performance (writing is slower though), with the advantage that if one drive fails, you don't lose anything. The disadvantage is that you only get one drive's worth of storage. Raid 0+1 is a combination of these two techniques that uses four drives. The data is striped across two drives, and each of these drives is mirrored, resulting in good performance and security in the case of drive failure. Still, you end up wasting two drives of data here. That's why I chose RAID 5. RAID 5 uses some special parity math and rotates the use of the drives to attain a nice balance between security, performance, and disk utilization. My RAID 5 setup uses four disks, and if any one of those disks fails I can replace it and not lose any data. (If two drives fail, I lose some data, so it is important to watch for a drive failure and deal with it immediately). With my four 320GB drives installed my RAID 5 setup provides just under 1TB of storage.</div><div><br /></div><div>Now that I've got it setup, I'm kicking myself for not doing this sooner. The drive performance is great, and it is nice to know that I have some security in the unfortunate event of a drive failure. My motherboard has six SATA ports, and it can have two RAID arrays. I'm considering modifying my setup to use a two disk RAID-1 for the operating system and applications and the four disk RAID-5 for all data storage. As cheap as hard drives are ($60 for 750GB!) it's seems silly not to take advantage of the security and performance afforded by a RAID configuration. The next thing I need to do is adhere to a regular backup schedule.</div><div><br /></div><div>A note: building the RAID (initializing it for the first time) took a long time. I kicked off the build process last night and it took just over 10 hours to complete. So if you are considering setting one up yourself, be sure to allow for that build time.</div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com1tag:blogger.com,1999:blog-8743735145097954046.post-8202893244122315412009-02-27T10:11:00.000-05:002010-02-22T09:22:57.014-05:00Windows Mobile 6.5 - Follow-UpA couple of weeks ago I posted an <a href="http://onesadjam.blogspot.com/2009/02/windows-mobile-65.html">article</a> criticizing Microsoft for the lack of innovation in Windows Mobile 6.5. Engadget has had a flurry of <a href="http://www.engadgetmobile.com/tag/windows+mobile+6.5/">articles on the topic</a> over the past month, and there is an interesting trend in the comments. Most commenters are getting into heated fanboi arguments over which is better: iPhone, WM6.5, WebOS, etc. To me this argument misses the point of the chief criticism towards Microsoft and the Windows Mobile 6.5 offering. The point is not to compare WM6.5 with iPhone and others, but instead to compare WM6.5 with WM6.1 and previous versions. Microsoft has stated that WM6.5 will probably not be available until the end of the year, and at that time will only be available on new phones. So the question is, is WM6.5 enough of an improvement over WM6.1 to purchase a new phone?<div><br /></div><div>It is clear to me the answer is no. There simply isn't enough new here to warrant upgrading a phone to get the new OS, especially when considering the high cost of smart phones with or without contract. Microsoft really needs to do something to breath new life into Windows Mobile if they want consumers to upgrade to a new phone to get the OS.</div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com17tag:blogger.com,1999:blog-8743735145097954046.post-87860234283355441802009-02-27T09:38:00.000-05:002010-02-22T09:22:57.018-05:00Is the OS Relevant Anymore?I just read a <a href="http://lnoldan.com/wp-trackback.php?p=39">post</a> from <a href="http://twitter.com/absenth">@absenth</a> referencing an article in <a href="http://www.linuxjournal.com/content/linux-desktop-who-cares">Linux Journal</a> about the relevancy of the OS. The crux of the article is that, due to the transition to Cloud Computing and Software as a Service, the host OS is becoming less important. Users are less concerned over the version of Windows or MacOS that the system is running, and more concerned with finding a good web browser and an office suite. This trend is most visible in the netbook arena where most offerings include a stripped down version of Linux at a reduced price. <div><br /></div><div>So is the OS relevant? If you were presented with a new laptop, and you had your choice of running Windows, MacOS, or Linux, would you have a preference? How much would you pay for your preference? I don't have any hard evidence, but I suspect that the average person would pick Windows if price were not a factor. That would be my choice. It would also be my choice when choosing an OS for a family member or non-technical friend. I know my family members are familiar with Windows, and familiarity means fewer calls to me for tech support. That's worth at least $30-$60 from me. On the other hand, I would choose the Linux variant for myself if it meant saving $100. </div><div><br /></div><div>I like the trend towards cloud computing, and I think it can only mean good things for consumers as the OS and hardware become less important.</div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com3tag:blogger.com,1999:blog-8743735145097954046.post-55585100783813575592009-02-19T23:06:00.000-05:002010-02-22T09:22:57.020-05:00Experiment: Pico-ITX as Hulu ClientI tried a little experiment tonight. I wanted to see if my <a href="http://www.via.com.tw/en/products/embedded/artigo/">Artigo Pico-ITX</a> machine could act as a <a href="http://www.hulu.com/">Hulu</a> client. I cleared off the machine and installed <a href="http://www.ubuntu.com/">Ubuntu 8.10</a>. After installing the flash plug-in for Firefox I pulled up Hulu and loaded a 30 second clip from Family Guy. The audio was perfect, but the video was very choppy. I checked the resource monitor and the CPU was just getting hammered. I guess I'm a little disappointed that even with a 1GHz processor and 1GB of memory, this little machine can't display streaming video. YouTube suffers from the same stutter. This is unfortunate, as I had hoped to connect this box to my HDTV as a quick and dirty web video streamer. Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0tag:blogger.com,1999:blog-8743735145097954046.post-84617785607464402712009-02-17T13:01:00.000-05:002010-02-22T09:22:57.022-05:00Windows Mobile 6.5<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWV82iivbN3wuzYfYGKxeTtbfEVodExUu5RKaXIZ9dUyP0MWYIBXOm7cHKAY9MylMg2WNJ03xAVLujHItmpB7ESePayTCiDV1aOs9QDi0GoXPQ7pQGuME2G-fIrsinQHs_VQ8T7ubJiQrH/s1600-h/winmo65-xperia-x1-1.jpg"><img style="cursor:pointer; cursor:hand;width: 400px; height: 240px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWV82iivbN3wuzYfYGKxeTtbfEVodExUu5RKaXIZ9dUyP0MWYIBXOm7cHKAY9MylMg2WNJ03xAVLujHItmpB7ESePayTCiDV1aOs9QDi0GoXPQ7pQGuME2G-fIrsinQHs_VQ8T7ubJiQrH/s400/winmo65-xperia-x1-1.jpg" border="0" alt="" id="BLOGGER_PHOTO_ID_5303828505027151874" /></a><br /><div>The Mobile World Congress (MWC) is underway, and all your favorite cell phone makers are there pitching the future of their products. Microsoft is at the event in a big way to introduce Windows Mobile 6.5. This facelift to WinMo 6.1 is hyped as the new, touch friendly version of the interface.</div><div><br /></div><div>I'm not very excited about this update. I think MS is right to call this a 6.5 rather than a 7.0. There just isn't enough there. And to make matters worse, it doesn't achieve the goal of making the interface a one-handed touch only affair. </div><div><br /></div><div>When I first purchased my PDA, it was running Windows Mobile 2003 SE. I used the WM 5.0 upgrade when it was available, and recently upgraded to WM 6.1. So what has changed in the Windows Mobile experience in the last 6 years? Very little, to be quite honest. If you have an old Windows Mobile PDA collecting dust in a drawer somewhere you could pick up the WinMo 6.5 devices on display and feel right at home. The only significant change that I have noticed is that I am not required to soft reset my device on a daily or weekly basis anymore. Taking six years to simply get the device to work is a sorry excuse for progress. It is time for Microsoft to really rethink how to approach the mobile market. If not, Apple, Nokia, Palm, and others are going to bury them with the smart innovations they are putting into their latest phones.</div>Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0tag:blogger.com,1999:blog-8743735145097954046.post-51056127096239415392009-02-09T14:45:00.000-05:002010-02-22T09:22:57.024-05:00Google SyncAfter I gave up my BlackBerry, I switched back to using my Dell Axim PDA. I'm very happy with it, and I'm having great luck with Windows Mobile 6.1 One thing that I have struggled with is to find a way to synchronize my mobile calendar, contacts, and e-mail with my Google calendar, contacts, and e-mail. Actually, I should not include e-mail, as that was always easy to do. You can synchronize either via POP or IMAP, whichever you prefer.<br /><br />So next up was calendar synchronization. My goal was to find a solution that would synchronize my work calendar with my google calendar, and both my Google and work calendars onto my PDA. The first app I tried for this was <a href="http://googlesystem.blogspot.com/2007/01/google-calendar-sync-for-windows.html">Google Calendar Sync for Mobile Devices</a> (GCSfMD). This is an application that runs on a Windows Mobile 5.0 or later device and synchronizes the calendar data on the PDA with the Google Calendar. The advantage here is that my work calendar would sync to my device, and then GCSfMD would modify the calendar to also include my Google Calendar appointments. Unfortunately GCSfMD is a one way sync. If I change an appointment on my Google Calendar my device is updated, but if I change an appointment on my device, the Google Calendar is unaltered. So my next attempt was to use <a href="http://www.google.com/support/calendar/bin/answer.py?hl=en&answer=89955">Google Calendar Sync</a>. Although the two have very similar names, they behave in very different ways. Google Calendar Sync synchronizes your Outlook calendar directly with your Google Calendar. It is a two-way sync, so if you update either your Outlook Calendar or your Google Calendar, the updates are synchronized between both. This is a great app, and I have gotten great use out of it the last few weeks.<br /><br />This left contact synchronization. There simply is no good solution for synchronizing Outlook Contacts and Google Contacts. Likewise, the only application that I found that will synchronize my mobile contacts with Google Contacts is <a href="http://oggsync.com/">OggSync</a>.<br /><br />Fortunately, today Google released <a href="http://www.google.com/mobile/default/sync.html">Google Sync</a>. Google Sync works with Windows Mobile and iPhone devices to synchronize both the Calendar and Contacts with your Google data. Best of all, it required no installation on my PDA! I simply modified my ActiveSync setup to point to the Google Mobile server, and everything synced up perfectly.<br /><br />A couple of questions you might have:<br /><ul style="font-weight: bold;"><li>Why not just use the device web browser to check the Google sites directly?</li></ul>That would be perfectly acceptable...if my device were a phone. It isn't, and I don't want to pay the extraordinary rates carriers are asking for all you can eat data service. By synchronizing my device, the data is available offline. So even if I'm in the car I still have access to all my calendar, contact, and e-mail data.<br /><ul style="font-weight: bold;"><li>How do you merge the data?</li></ul>I still need Google Calendar Sync. Google Sync will sync your device data with your Google data, but it will not merge with your Outlook data. I still use Google Calendar Sync to merge my Google Calendar with my Outlook calendar. There is still no solution for merging my contact data, but I'm okay with this because I can live with only having my personal contacts on my device.<br /><ul style="font-weight: bold;"><li>Can I sync my device with both Google and Outlook?</li></ul>At this time, no. Windows Mobile and iPhone both restrict you to a single synchronization source. You'll need to decide how you want to approach it. For me, I've chosen to make Google my primary data provider, and I sync all of the other data with Google. You may prefer to sync with your Outlook data, in which case you will need to find alternative applications to sync up.<br /><br />In an ideal world, my device would sync and store all of my various calendars, contacts, e-mail, and data from as many sources as I wanted. Until then, the combination of Google Sync and Google Calendar Sync will fill the void.Adam Joneshttp://www.blogger.com/profile/03047845969000890121noreply@blogger.com0