Perfil de JonathanJon's Bing Maps NotesBlogListas Herramientas Ayuda

Jonathan Ng

Resources for extending Bing Maps functionality

Jon's Bing Maps Notes

28 octubre

Drawing Pushpins onto a Map Image in PHP

The Bing Maps Imagery service can be used to return an image centered over a specific coordinate, but if you want to mark that coordinate on the image, the Imagery service does not provide a simple method to do this. Similar to placing pushpins in .NET, the pushpin must be drawn onto the map image using the methods available to us. In PHP, this is usually done using the GD or ImageMagick libraries. In this example, the GD library has been used to place a pushpin onto the map.

Adding to the previous PHP example where an image was retrieved, the following code places a pushpin onto the map.

$mapLatCenter = $lat;
$mapLonCenter = $lon;
$zoom = $options['ZoomLevel'];
$image = imagecreatefrompng($mapUriResponse->GetMapUriResult->Uri);
$mapWidth = imagesx($image);
$mapHeight = imagesy($image);
$dstImage = imagecreatetruecolor(imagesx($image),imagesy($image));
$pinImage=imagecreatefromgif("pushpin.gif");
$x = 0;
$y = 0;
LatLongToPixel($x, $y, $lat, $lon, $mapLatCenter, $mapLonCenter, $zoom, $mapWidth, $mapHeight);
imagecopymerge($dstImage,$image,0,0,0,0,imagesx($image),imagesy($image),100);
imagecopymerge($dstImage,$pinImage,$x,$y,0,0,imagesx($pinImage),imagesy($pinImage),100);
 
imagepng($dstImage, "mappin.png");

This snippet creates a PNG image that is saved to the local directory. The image can be displayed using

<img src="mappin.png"/>

The GD Library

Some functions to look out for in the GD library.

The imagecreatetruecolor function is important for keeping the proper colors of images added to each other. Image formats such as GIF may not look correct if added directly to another image. The GIF’s colors will use the indexed palette of the other image, so something that is blue in the GIF may be red because the palette contains different color indices. This function creates an image buffer in true color, so it does not depend on a predefined palette.

The imagecreatefrompng and imagecreatefromgif functions are pretty self-explanatory. To load a PNG image, use imagecreatefrompng and load a GIF using imagecreatefromgif. JPEG is also supported.

The imagecopymerge function is used to merge two images together. This is also the only function that copies transparency information. Transparent GIFs were quite easy to add to the map. PNGs with alpha channel should also work in GD, but getting the alpha channel information to stay is quite an exercise and could not work in this example. Imagemagick may be a better choice for PNGs, so that will be next on the list.

A complete sample of this code can be found at http://cid-0cda7ef602f8cca1.skydrive.live.com/self.aspx/.Public/imageryservicewithpin.zip.

18 septiembre

Javadocs for Bing Maps Web Services

In case my examples don’t provide enough functionality, it is always possible to access the web service methods directly. All of the available methods are listed on the MSDN, but there are some Java-specific methods, most notably the get and set methods for most of the properties. Instead of searching through the source code, I have rolled up all the methods into a Javadoc. Feel free to use it as you see fit.

http://cid-0cda7ef602f8cca1.skydrive.live.com/self.aspx/.Public/VEWS-Javadoc.zip

14 septiembre

Bing Maps Web Services in PHP

Many people have asked how to access the web services using PHP. Since PHP has a SOAP extension, it should be possible. The example makes use of the Geocode and Imagery services of the Bing Maps Web Services.

Download the code sample.

Although PHP has a command-line interface available, this example requires the use of a PHP-enabled web server with the php_soap extension enabled. The following steps will install a local web server for development purposes.

1. Download WampServer. This is available from http://www.wampserver.com/en/. Install it to the default location or a location of your choice.

2. After the WampServer is installed, left-click on the WampServer icon in the system tray. Hover over PHP->PHP Extensions and select php_soap from the list.

3. The server will automatically restart. Left-click on the icon again and select “www Directory”. This is where the web pages are stored. Store the php file there.

At this point, the server has been installed and ready to run the example. Download the Token Service WSDL and Bing Web Service WSDLs from their respective sites. Save them to a location that is accessible to the www folder. They can be saved in the same folder as the php files. (For security purposes, access to these files should be blocked to Internet users in a live environment). For this example, the WSDL files are expected to be in the same directory as the php file.

In order to use the Imagery service, a small modification must be made to the WSDL file. The third line of the WSDL should be

<wsdl:import namespace="http://dev.virtualearth.net/webservices/v1/imagery/contracts" location="http://staging.dev.virtualearth.net/webservices/v1/metadata/imageryservice/imageryservice1.wsdl" />

This needs to be changed to

<wsdl:import namespace="http://dev.virtualearth.net/webservices/v1/imagery/contracts" location="http://dev.virtualearth.net/webservices/v1/metadata/imageryservice/imageryservice1.wsdl" />

The word “staging” was removed from the URL in the location attribute. This resolves a problem in the staging WSDL that causes an error involving an unexpected namespace declaration.

To run the example, open up a web browser and point it to http://localhost/imageryservicesample.php. The example requires staging credentials and address. Improper inputs are not checked, but your application should verify for reasonable inputs. Zoom level is optional and will return a default zoom level if none is entered. Press Submit to see the map image and geocode coordinates that were returned.


11 septiembre

Drawing a Route Path onto the Map Image in Java

We will be taking a look at using the Route and Imagery Web Services. This article draws largely on Richard Brundritt’s post on Drawing Routes with the VE Web Service in C#.

Download the code for this example.

To do this job, we need to get the Route and Imagery services set up. In the last article, we set up the Geocode service. Creating the service stubs for Route and Imagery is just copy/paste and changing Geocode to Route or Imagery. If you used the ant script to download the WSDLs, all the necessary classes will be available in the JAR that was created.

I also created classes to handle creation of the stub and setup of the service client. The classes also handle the repetitive code consisting of the requests and responses. Feel free to customize them as needed.

RouteWrapper.java

import net.virtualearth.dev.webservices.v1.route.*;
import net.virtualearth.dev.webservices.v1.common.*;

public class RouteWrapper extends VEServiceWrapper
{
private BasicHttpBinding_IRouteServiceStub routeService;

public RouteWrapper()
{
try
{
routeService = (BasicHttpBinding_IRouteServiceStub) (new RouteServiceLocator()).getBasicHttpBinding_IRouteService();
}
catch(Exception ex)
{
System.err.println("Exception: " + ex);
}
}



public Waypoint[] getRouteFromMajorRoads(Location stop) throws Exception
{
MajorRoutesOptions routeOptions = null;
Waypoint endPoint = new Waypoint("", stop);
MajorRoutesRequest request = new MajorRoutesRequest(credentials, "EN-us", null, null, endPoint, routeOptions);
response = routeService.calculateRoutesFromMajorRoads(request);
return ((MajorRoutesResponse)response).getStartingPoints();
}

public RouteResult getRoute(Location[] stops) throws Exception
{
Waypoint[] waypoints = new Waypoint[stops.length];
for(int i=0; i < stops.length; i++)
{
waypoints[i] = new Waypoint();
waypoints[i].setLocation(stops[i]);
}
RouteOptions routeOptions = new RouteOptions();
routeOptions.setRoutePathType(RoutePathType.Points);
RouteRequest request = new RouteRequest(credentials, "EN-us", null, null, routeOptions, waypoints);
response = routeService.calculateRoute(request);
return ((RouteResponse)response).getResult();
}
}


ImageryWrapper.java

I decided to also put into the ImageryWrapper the method to draw a route onto a map. It is related to the image that is returned, and not the route, so it goes in the image-related class. You may want to separate it, but remember to make appropriate changes to object references.

import net.virtualearth.dev.webservices.v1.imagery.*;
import net.virtualearth.dev.webservices.v1.common.*;
import net.virtualearth.dev.webservices.v1.route.*;

import javax.swing.ImageIcon;
import javax.imageio.*;
import java.net.URL;
import java.awt.*;
import java.awt.image.*;
import java.awt.geom.*;

public class ImageryWrapper extends VEServiceWrapper
{
private BasicHttpBinding_IImageryServiceStub imageryService;

public ImageryWrapper()
{
try
{
imageryService = (BasicHttpBinding_IImageryServiceStub) (new ImageryServiceLocator()).getBasicHttpBinding_IImageryService();
}
catch(Exception ex)
{
System.err.println("Exception: " + ex);
}
}

public String getImageURL(Location loc) throws Exception
{
MapUriOptions options = new MapUriOptions();
MapUriRequest request = new MapUriRequest(
credentials,
"EN-us",
null, // executionOptions
null, // userProfile
loc, // center
null, //majorRoutesDestination
options,
null); //pushpins

response = imageryService.getMapUri(request);
return ((MapUriResponse) response).getUri();
}

public ImageIcon getImage(Location loc) throws Exception
{
// We already have a method to get the URL, so use that
String imageUrl = getImageURL(loc);
ImageIcon map = new ImageIcon(new URL(imageUrl));
return map;
}

public ImageIcon getImageRoutesFromMajorRoads(Location loc) throws Exception
{
MapUriOptions options = new MapUriOptions();
MapUriRequest request = new MapUriRequest(
credentials,
"EN-us",
null, // executionOptions
null, // userProfile
null, // center
loc, //majorRoutesDestination
options,
null); //pushpins
MapUriResponse response = imageryService.getMapUri(request);
return new ImageIcon(new URL(((MapUriResponse)response).getUri()));
}

public ImageIcon plotRoute(RouteResult routeResult) throws Exception
{
BestView view = VETools.CalculateMapView(routeResult.getRoutePath().getPoints());
MapUriOptions options = new MapUriOptions();
options.setZoomLevel(view.zoom);
options.setImageSize(new SizeOfint((int)VETools.mapHeight, (int)VETools.mapWidth));
MapUriRequest request = new MapUriRequest();
request.setCredentials(credentials);
request.setCenter(view.center);
request.setOptions(options);
response = imageryService.getMapUri(request);
BufferedImage map = ImageIO.read(new URL(((MapUriResponse) response).getUri()));
// Draw the route on the buffer
Path2D path = new Path2D.Double();
Location[] stops = routeResult.getRoutePath().getPoints();
Point2D point = VETools.LatLongToPixel(stops[0],view);
path.moveTo(point.getX(),point.getY());
for(int i = 1; i < stops.length; i++)
{
point = VETools.LatLongToPixel(stops[i],view);
path.lineTo(point.getX(),point.getY());
}
Graphics2D painter = map.createGraphics();
painter.setPaint(new Color(Color.RED.getRGB()));
painter.setStroke(new BasicStroke(3));
painter.draw(path);
return new ImageIcon(map);
}
}

The plotRoute method takes a RouteResult object as a parameter.

public ImageIcon plotRoute(RouteResult routeResult) throws Exception
{
BestView view = VETools.CalculateMapView(routeResult.getRoutePath().getPoints());
MapUriOptions options = new MapUriOptions();
options.setZoomLevel(view.zoom);
options.setImageSize(new SizeOfint((int)VETools.mapHeight, (int)VETools.mapWidth));
MapUriRequest request = new MapUriRequest();
request.setCredentials(credentials);
request.setCenter(view.center);
request.setOptions(options);
response = imageryService.getMapUri(request);
BufferedImage map = ImageIO.read(new URL(((MapUriResponse) response).getUri()));
// Draw the route on the buffer
Path2D path = new Path2D.Double();
Location[] stops = routeResult.getRoutePath().getPoints();
Point2D point = VETools.LatLongToPixel(stops[0],view);
path.moveTo(point.getX(),point.getY());
for(int i = 1; i < stops.length; i++)
{
point = VETools.LatLongToPixel(stops[i],view);
path.lineTo(point.getX(),point.getY());
}
Graphics2D painter = map.createGraphics();
painter.setPaint(new Color(Color.RED.getRGB()));
painter.setStroke(new BasicStroke(3));
painter.draw(path);
return new ImageIcon(map);
}
 

VETools.java

Some methods that will be useful in various calculations. Thanks to Richard Brundritt for posting the formulae in his blog.

import net.virtualearth.dev.webservices.v1.common.*;
import java.awt.geom.*;
import java.awt.Point;

public class VETools
{
private static final double earthR = 6371; //radius in km
static double mapHeight = 600;
static double mapWidth = 900;

public static double HaversineDistance(double lat1, double lon1, double lat2, double lon2)
{
double factor = Math.PI / 180;
double dLat = (lat2-lat1)*factor;
double dLon = (lon2-lon1)*factor;
double a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1*factor)
* Math.cos(lat2*factor) * Math.sin(dLon/2) * Math.sin(dLon/2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return earthR*c;
}



public static BestView CalculateMapView(Location[] points)
{
//Calculate bounding rectangle
double maxLat = -90, minLat = 90, maxLon = -180, minLon = 180;

//default zoom scales in km/pixel from http://msdn2.microsoft.com/en-us/library/aa940990.aspx
double[] defaultScales = { 78.27152, 39.13576, 19.56788, 9.78394, 4.89197, 2.44598,
1.22299, 0.61150, 0.30575, 0.15287, .07644, 0.03822, 0.01911, 0.00955, 0.00478,
0.00239, 0.00119, 0.0006, 0.0003 };

//Calculate bounding box for array of locations
for (int i = 0; i < points.length; i++)
{
if (points[i].getLatitude() > maxLat)
maxLat = points[i].getLatitude();

if (points[i].getLatitude() < minLat)
minLat = points[i].getLatitude();

if (points[i].getLongitude() > maxLon)
maxLon = points[i].getLongitude();

if (points[i].getLongitude() < minLon)
minLon = points[i].getLongitude();
}

//calculate center coordinate of bounding box
double centerLat = (maxLat + minLat) / 2;
double centerLong = (maxLon + minLon) / 2;

//create a Location object for the center point
Location centerPoint = new Location();
centerPoint.setLatitude(centerLat);
centerPoint.setLongitude(centerLong);

//want to calculate the distance in km along the center latitude between the two longitudes
double meanDistanceX = HaversineDistance(centerLat, minLon, centerLat, maxLon);

//want to calculate the distance in km along the center longitude between the two latitudes
double meanDistanceY = HaversineDistance(maxLat, centerLong, minLat, centerLong) * 2;

//calculates the x and y scales
double meanScaleValueX = meanDistanceX / mapWidth;
double meanScaleValueY = meanDistanceY / mapHeight;

double meanScale;

//gets the largest scale value to work with
if (meanScaleValueX > meanScaleValueY)
meanScale = meanScaleValueX;
else
meanScale = meanScaleValueY;

//intialize zoom level variable
int zoom = 1;

//calculate zoom level
for (int i = 1; i < 19; i++)
{
if (meanScale >= defaultScales[i])
{
zoom = i;
break;
}
}

//return a BestView object with the center point and zoom level to use
return new BestView(centerPoint, zoom);
}

public static Point LatLongToPixel(Location latlong, BestView view)
{
//Formulas based on following article:
//http://msdn.microsoft.com/en-us/library/bb259689.aspx

//calcuate pixel coordinates of center point of map
double sinLatitudeCenter = Math.sin(view.center.getLatitude() * Math.PI / 180);
double pixelXCenter = ((view.center.getLongitude() + 180) / 360)
* 256 * Math.pow(2, view.zoom);
double pixelYCenter = (0.5 - Math.log((1 + sinLatitudeCenter)
/ (1 - sinLatitudeCenter)) / (4 * Math.PI)) * 256 * Math.pow(2, view.zoom);

//calculate pixel coordinate of location
double sinLatitude = Math.sin(latlong.getLatitude() * Math.PI / 180);
double pixelX = ((latlong.getLongitude() + 180) / 360) * 256 * Math.pow(2, view.zoom);
double pixelY = (0.5 - Math.log((1 + sinLatitude) / (1 - sinLatitude))
/ (4 * Math.PI)) * 256 * Math.pow(2, view.zoom);

//calculate top left corner pixel coordiates of map image
double topLeftPixelX = pixelXCenter - (mapWidth / 2);
double topLeftPixelY = pixelYCenter - (mapHeight / 2);

//calculate relative pixel cooridnates of location
double x = pixelX - topLeftPixelX;
double y = pixelY - topLeftPixelY;

return new Point((int)Math.floor(x), (int)Math.floor(y));
}
}



class BestView
{
public int zoom;
public Location center;

public BestView(Location c, int z)
{
zoom = z;
center = c;
}
}


ImageRouteTest.java
 
import org.apache.axis.*;
import net.virtualearth.dev.webservices.v1.common.*;
import net.virtualearth.dev.webservices.v1.geocode.*;
import net.virtualearth.dev.webservices.v1.route.*;
import net.virtualearth.dev.webservices.v1.imagery.*;

import javax.swing.*;
import java.net.URL;
import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
import java.awt.image.*;

public class ImageRouteTest
{
public static void main(String[] args) throws Exception
{
String token = Vetoken.getToken("VEUsername","VEPassword");
GeocodeWrapper gw = new GeocodeWrapper();
ImageryWrapper iw = new ImageryWrapper();
gw.setToken(token);
iw.setToken(token);

GeocodeResult[] start = gw.geocode("1295 Military Trail, Scarborough, ON", null);
GeocodeResult[] end = gw.geocode("1 Blue Jays Way, Toronto, ON", null);
RouteWrapper rw = new RouteWrapper();
rw.setToken(token);
Location[] path = new Location[2];
path[0] = start[0].getLocations()[0];
path[1] = end[0].getLocations()[0];
RouteResult result = rw.getRoute(path);
ImageIcon icon = iw.plotRoute(result);
String directions = "";
RouteLeg[] legs = result.getLegs();
for(int i=0; i < legs.length; i++)
{
ItineraryItem[] items = legs[i].getItinerary();
for(int j=0; j < items.length; j++)
{
directions += items[j].getText() + "\n";
}
}
// Regex to get rid of the markup formatting tags
directions = directions.replaceAll("<[/a-zA-Z:]*>","");

JFrame frame = new JFrame("ImageRouteTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel,BoxLayout.PAGE_AXIS));
JLabel map = new JLabel(icon);
map.setAlignmentX(Component.CENTER_ALIGNMENT);
panel.add(map);
JTextArea text = new JTextArea("How to get to a Jays game\n\n" + directions);
text.setAlignmentX(Component.CENTER_ALIGNMENT);
panel.add(text);

frame.setContentPane(panel);
frame.pack();
frame.setVisible(true);
}
}

Output

I am not a big Jays fan, or even a big baseball fan, but everyone eventually goes to the ballpark. Let’s say I had to get to the ballpark from my old school.

imageroute

Conclusion

As with the C# version of the Imagery service, the map is static so it cannot be panned and zoomed. As a result, its usefulness is somewhat limited, but it is definitely possible to zoom to a specific point on the route, for example. As always, suggestions for improvements are welcome.

09 septiembre

Bing Maps Web Services in Java

We often find ourselves performing the same steps over and over. At least for testing purposes, it is a good idea to have a set of prepared functions to save time rewriting something you’ve done many times before. We saw last time that creating the stub for the web services required explicit casting to a rather lengthy class type, and calling a method that has no equivalent in the .NET architecture. Besides simplifying the code, you can add functions that you commonly use.

With this in mind, I wrote a “wrapper” class for each of the web services provided by Bing Maps: geocode, search, imagery, route, and token. This article focuses on the geocode service of Bing Maps Web Services.

Note: I do not perform any error checking or exception handling aside from checking whether the token was set prior to calling methods. Be sure to have adequate exception handling before placing anything into a production environment.

GeocodeWrapper.java

import org.apache.axis.*;
import net.virtualearth.dev.webservices.v1.geocode.*;
import net.virtualearth.dev.webservices.v1.common.*;

public class GeocodeWrapper extends VEServiceWrapper
{
private BasicHttpBinding_IGeocodeServiceStub geocodeService;

public GeocodeWrapper()
{
try
{
geocodeService = (BasicHttpBinding_IGeocodeServiceStub) (new GeocodeServiceLocator()).getBasicHttpBinding_IGeocodeService();
}
catch(Exception ex)
{
System.err.println("Exception: " + ex);
}
}

public GeocodeResult[] geocode(String query, Address address, GeocodeOptions options)
throws Exception
{
if(credentials == null)
throw new Exception("Credentials not set");

// Create the Geocode Service Client
GeocodeRequest request = new GeocodeRequest(credentials,"EN-us",null,null,address,options,query);
response = geocodeService.geocode(request);
GeocodeResult[] results = ((GeocodeResponse)response).getResults();

return results;
}

public GeocodeResult[] geocode(String query, GeocodeOptions options)
throws Exception
{
return geocode(query, null, options);
}

public GeocodeResult[] reverseGeocode(Location latlong, ExecutionOptions options) throws Exception
{
if(credentials == null)
throw new Exception("Credentials not set");

// Create the Geocode Service Client
ReverseGeocodeRequest request = new ReverseGeocodeRequest(credentials,"EN-us",options,null,latlong);
response = geocodeService.reverseGeocode(request);
GeocodeResult[] results = response.getResults();
return results;
}

public GeocodeResponse getResponse()
{
return (GeocodeResponse) response;
}
}


 

To geocode an address using this class, the 3 steps needed (request, response, results) are reduced to one line of code. I never change user options or execution options, so they are permanently set to null. The geocode() method is overloaded, in case I ever want to geocode an Address object rather than a String of the address.

The VEServiceWrapper base class contains what is common to all classes. At the moment, only the Credentials and the ResponseBase objects are shared between classes.

VEServiceWrapper.java

import net.virtualearth.dev.webservices.v1.common.*;

public abstract class VEServiceWrapper
{
protected Credentials credentials = null;
protected ResponseBase response;

public void setToken(String token)
{
credentials = new Credentials("1",token);
}

public String getToken()
{
return credentials.getToken();
}

public Credentials getCredentials()
{
return credentials;
}
}

These are available for download from http://cid-0cda7ef602f8cca1.skydrive.live.com/self.aspx/.Public/Wrappers.zip