I recently started playing around with Google Maps and Geocoding. I wanted to create a map on my main site that would show the cities that our customers are in. This meant manipulating a Google Map with markers.
After a quick search, I ran across Google's own documentation for this. It turns out it really isn't that difficult. You just need to provide latitude and longitude points for the markers to be placed on the maps. See the Google Maps APIs Documentation. A list of marker images can be found at https://sites.google.com/site/gmapsdevelopment/.
To get the latitude and longitude from the data you most likely have stored (ie, address, city, state, zip country) you need to use Google's Geocoding API. I at first thought about just doing this on the fly, but the GeoCoding API has quotas (2,500 a day) and doing that with each page load would not only be super slow, but the quota would be reached in the first couple page displays. So, instead I decided to convert the address info to latitude and longitude data once.
In order to get a list of latitude and longitude markers I first created a table to hold that data:
Sidebar: Curious about the Terms of Service for Google Geocoding? I was too, wondering if I could store the data. From what is understood after a lot of searching is you can store the data as long as it is only used with Google's apps and not others (like Bing or Yahoo). Here's a link to a good discussion on this. If you feel this is a misinterpretation please feel free to contact me. |
File Name . . . . CSTLLPF
Library . . . . BVSCOMP
Format Descr . .
Format Name . . . RCSTLL
File Type . . . . PF Unique Keys - N
Field Name FMT Start Lngth Dec Key Field Description
LLLAT P 1 9 06 Latitude
LLLNG P 6 9 06 Longitude
LLCOUNT P 11 13 00 Count
LLFMTADD A 18 256 Formatted Address
For this file I made a key on the Latitude and Longitude fields so that if there was more than one customer in that city I could display that count on the marker.
Next I ran through my customer master file and for each location attempted to retrieve the geocoding data for the location. To do this you can use the Google Maps Geocoding API. This is a web service that you can pass address information to and retrieve the latitude and longitude data required. This API needs to be enabled in your Google API Manager Console. You will need to create a project and request a key for the use of the API. There is also a limit per day of 2,500 requests per day.
The JSON data is returned from the API in the following format:
{
"results" : [
{
"address_components" : [
{
"long_name" : "Riccione",
"short_name" : "Riccione",
"types" : [ "locality", "political" ]
},
{
"long_name" : "Riccione",
"short_name" : "Riccione",
"types" : [ "administrative_area_level_3", "political" ]
},
{
"long_name" : "Province of Rimini",
"short_name" : "RN",
"types" : [ "administrative_area_level_2", "political" ]
},
{
"long_name" : "Emilia-Romagna",
"short_name" : "Emilia-Romagna",
"types" : [ "administrative_area_level_1", "political" ]
},
{
"long_name" : "Italy",
"short_name" : "IT",
"types" : [ "country", "political" ]
},
{
"long_name" : "47838",
"short_name" : "47838",
"types" : [ "postal_code" ]
}
],
"formatted_address" : "47838 Riccione, Province of Rimini, Italy",
"geometry" : {
"bounds" : {
"northeast" : {
"lat" : 44.0253177,
"lng" : 12.6839613
},
"southwest" : {
"lat" : 43.9801,
"lng" : 12.6136248
}
},
"location" : {
"lat" : 43.999296,
"lng" : 12.6555485
},
"location_type" : "APPROXIMATE",
"viewport" : {
"northeast" : {
"lat" : 44.0253177,
"lng" : 12.6839613
},
"southwest" : {
"lat" : 43.9801,
"lng" : 12.6136248
}
}
},
"place_id" : "ChIJe-91HWHdLBMRJ4yGKB1Dum8",
"types" : [ "locality", "political" ]
}
],
"status" : "OK"
}
The program to gather the geocoding data and populate our database is as follows:
H DFTACTGRP(*NO) BNDDIR('BVSTOOLS')
****************************************************************
FCSTMSTLL UF E K DISK
FCSTLL1 UF A E K DISK USROPN
****************************************************************
* Prototypes *
****************************************************************
/include QCOPYSRC,yajl_h
/COPY QCOPYSRC,GETURICOPY
*
D GetUriRG pr extpgm('GETURIRG')
D G_In like(GetUri_In)
D G_Out like(GetUri_Out)
D G_Head like(GetUri_Head)
D G_Data like(GetUri_Data)
D G_MsgCd like(GetUri_MsgCd)
D G_Msg like(GetUri_Msg)
*
D #QCmdExc PR ExtPgm('QCMDEXC')
D Cmd 32000 Const
D CmdLen 15 5 Const
****************************************************************
* Work variables
D docNode s like(yajl_val)
D resultsList s like(yajl_val)
D node s like(yajl_val)
D addressNode s like(yajl_val)
D geometryNode s like(yajl_val)
D errorMsgNode s like(yajl_val)
D locationNode s like(yajl_val)
D val s like(yajl_val)
D yajl_errMsg s 500a varying inz('')
*
D WKLat S LIKE(LLLAT)
D WKLng S LIKE(LLLNG)
*
D thisLat S LIKE(LLLAT)
D thisLng S LIKE(LLLNG)
D thisAddress S LIKE(LLFMTADD)
*
D QCmdCmd S 32000
D i S 10i 0
D quit S N INZ(*OFF)
****************************************************************
/free
//CLRPFM FILE(BVSCOMP/CSTLLPF)
//CPYF FROMFILE(CSTMSTPF) TOFILE(CSTMSTLL) MBROPT(*REPLACE) FMTOPT(*MAP *DROP)
open CSTLL1;
read CSTMSTLL;
dow (not %eof(CSTMSTLL));
if (CMPRC <> 'Y');
EXSR $getLatLong;
if (thisLat <> 0) and (thisLng <> 0);
WKLat = thisLat;
WKLng = thisLng;
CHAIN LLKEY CSTLL1;
if (%found(CSTLL1));
LLCOUNT += 1;
UPDATE RCSTLL;
else;
LLLAT = WKLat;
LLLNG = WKLng;
LLCOUNT = 1;
LLFMTADD = thisAddress;
WRITE RCSTLL;
endif;
endif;
if (quit);
LEAVE;
else;
CMPRC = 'Y';
UPDATE RCSTMST;
endif;
endif;
read CSTMSTLL;
enddo;
close CSTLL1;
*INLR = *ON;
//***************************************************************
//* Get Lat and Long
//***************************************************************
begsr $GetLatLong;
thisLat = 0;
thisLng = 0;
Clear GetUri_In;
GI_URI = 'maps.googleapis.com/maps/api/geocode/json';
GI_Data = 'key=<mykey>' +
'&address=' +
%trim(CMFCITY) + ',' +
%trim(CMFSTATE) + ',' +
%trim(CMFCNT);
GI_ReqMeth = 'GET';
GI_OutType = '*RETURN';
//GI_OutType = '*STMF';
//GI_Stmf = '/tmp/geo.json';
GI_SSL = '*YES';
GI_Port = 443;
GI_SprHead = '*YES';
GI_CCSID = 1208;
GI_CODPAG = 1208;
//GI_Debug = '*YES';
GetUriRG(GetUri_In:GetUri_Out:GetUri_Head:GetUri_Data:
GetUri_MsgCd:GetUri_Msg);
docNode = yajl_buf_load_tree(%addr(GetUri_Out):
%len(%trim(GetUri_Out)):
yajl_errMsg);
if (docNode <> *NULL);
errorMsgNode = YAJL_object_find(docNode:'error_message');
if (errorMsgNode = *NULL);
resultsList = YAJL_object_find(docNode:'results');
i = 0;
dow YAJL_ARRAY_LOOP(resultsList:i:node);
addressNode = YAJL_object_find(node:'formatted_address');
thisAddress = yajl_get_string(addressNode);
geometryNode = YAJL_object_find(node:'geometry');
locationNode = YAJL_object_find(geometryNode:'location');
val = YAJL_object_find(locationNode: 'lat');
thisLat = yajl_get_number(val);
val = YAJL_object_find(locationNode: 'lng');
thisLng = yajl_get_number(val);
enddo;
else;
quit = *ON;
endif;
yajl_tree_free(docNode);
endif;
endsr;
/end-free
****************************************************************
* INZ *
****************************************************************
C *INZSR BEGSR
*
C LLKey KLIST
C KFLD WKLat
C KFLD WKLng
*
C ENDSR
The program is pretty simple. It reads through the customer master file (CSTMSTLL) and uses the address information to call the Google Geocoding API. The data is returned in JSON format and parsed using the invaluable IBM i port of YAJL from Scott Klement. Because this program is only going to be run once (or once every month or year to update the list) I didn't do a lot of error checking and used RLA instead of SQL. I may change that in the future. It's not of any consequence, though. Also, the reason I open and close the CSTLLPF file manually is I was thinking of adding some CL commands to clear the file if needed.
Once the program completes we have a full list of data to use in our web application. The data looks like the following:
LLLAT LLLNG LLCOUNT LLFMTADD
37.971559 87.571089- 3 Evansville, IN, USA
51.441641 5.469722 1 Eindhoven, Netherlands
34.147784 118.144515- 2 Pasadena, CA, USA
36.677737 121.655501- 1 Salinas, CA, USA
53.277962 110.006145- 2 Lloydminster, AB, Canada
40.904019 74.408740- 1 Boonton, NJ 07005, USA
42.069750 87.787840- 2 Glenview, IL, USA
49.166589 123.133569- 1 Richmond, BC, Canada
.....
The main processing for displaying the web page with the map and the markers will be done using jQuery which calls an eRPG (RPG CGI) program to return the geocoding data as a JSON object.
You will need to specify a link to a JavaScript source used by Google's Map application:
<script src="//maps.googleapis.com/maps/api/js?v=3&libraries=places&key=<mykey>"></script>
Yes, you need to supply your key here as well. The good news is the quotas for displaying maps using the Google Maps Javascript API is 25,000 a day.
The HTML for the web page is very simple. Shown here is the main part of the page that we use to define the maps DIV object where the map will be placed:
Here is a map with locations for most of our customers. Hover your mouse on a marker to see the count for that city/area.
<div id="map" style="width: auto; min-height: 500px; height: auto; border: 1px solid black;"></div>
The jQuery used when the page loads is as follows:
var centerPosition = {lat: 39.8282, lng: -98.5795};
var mapOptions = {
zoom: 2,
mapTypeId: google.maps.MapTypeId.ROADMAP,
center: centerPosition
};
$(document).ready(function() {
$("#map").html("Loading locations...");
$.post("/e-rpg/getcstll")
.fail(function(xhr, status, error) {
$("#map").html("Error loading map markers. " + error);
})
.done(function(data) {
doMap(data);
});
});
function doMap(cityList) {
map = new google.maps.Map(document.getElementById("map"), mapOptions);
var image = 'http://labs.google.com/ridefinder/images/mm_20_green.png';
$.each(cityList, function(index, value) {
var marker = new google.maps.Marker({
position: {lat:parseFloat(value.lat), lng:parseFloat(value.lng)},
title: value.title,
draggable: false,
map: map,
icon: image
});
});
}
The jQuery first calls program GETCSTLL which returns the geocoding data in the following JSON format.
[
{"lat": 33.2323200,
"lng": -223.113343,
"title": "City, State (count)"
},...
]
We then read through the JSON array results and for each result place a marker on the map.
The RPG program that builds the JSON geocoding data, GETCSTLL, appears as the following:
H DFTACTGRP(*NO) BNDDIR('ERPGSDK')
****************************************************************
* Prototypes *
****************************************************************
/COPY QCOPYSRC,P.LIBL
/COPY QCOPYSRC,P.SQL
/include qcopysrc,yajl_h
****************************************************************
D CSTLLDS E DS EXTNAME(CSTLLPF)
*
D errMsg S 500 Varying
****************************************************************
/free
exec sql Set Option Commit=*NONE;
#pushLib('BVSCOMP');
yajl_genOpen(*OFF);
yajl_beginArray();
exec sql declare C1 cursor for
select LLLAT, LLLNG, LLCOUNT, LLFMTADD from CSTLLPF;
exec sql open C1;
exec sql fetch from C1
into :LLLAT, :LLLNG, :LLCOUNT, :LLFMTADD;
dow (xSQLState2 = Success_On_Sql);
yajl_beginObj(); //Detail Object
yajl_addChar('lat':%char(LLLAT));
yajl_addChar('lng':%char(LLLNG));
yajl_addChar('title':%trimr(LLFMTADD) + ' (' + %char(LLCOUNT) + ')');
yajl_endObj(); //Detail Object
exec sql fetch from C1
into :LLLAT, :LLLNG, :LLCOUNT, :LLFMTADD;
enddo;
exec sql Close C1;
yajl_endArray();
yajl_endObj();
yajl_writeStdout(200:errMsg);
yajl_genClose();
#popLib('BVSCOMP');
*INLR = *on;
Again you can see we are using the YAJL product, but this time it is used to generate the JSON as well as write it out to standard output for the jQuery to use in it's processing.
One note: for the latitude and longitude I first was using yajl_addNum() to create them as numbers. The only problem was for negative numbers without a number in the ones place they did not have a leading zero (ie, they were -.0343 instead of -0.0343). This caused an issue with the JSON as without the leading zero the JSON is invalid. I was able to get around that by making the value a string and then converting it to a float data type when plugging the value into the map position parameter.
When done and working we know can see a map containing markers for each city, state. To see the count just hover your mouse pointer over the marker on the map.
This was a fun project for sure, and I'm sure there are many features that could be added. We shall see!