Skip to main content

Querying GitHub Issues from Google App Script

I'm currently exploring different integrations that can be done between Google App Script and Git. This is part of a bigger attempt to integrate Git data into my project management system. I'll post more on that later. GitHub supports a number of very nice sub-systems. One of them is the GitHub Issues ticket tracking system. It's fairly robust and comes with GitHub - which means it's highly integrated out-of-the-box. Integration with Google's App Script is done via the fetchURL command and JSON object manipulation.  After a good bit of experimentation, I've settled on querying GitHub Issues with an "on open spreadsheet event" to avoid issues with Google's quota system. Essentially, Google prevents you from calling the fetchURL command more than X times (see the quota limits ) per day. That's not great if you have people actively working and changing data in a spreadsheet. Some of my other App Script routines are running thousands of times per d...

MapQuest and Delphi

A few weeks ago I started down the path of discovering how to GeoCode addresses for Delphi. This is related to a project we had where we wanted to show a map of a given point and then show facilities located near it on a map. I had originally started working with maps.live.com (which is a MS site). They have a decent little interface for JS that seemed to work for what I wanted AND I didn't have to store the annoying little GeoCoding addresses anywhere. This works great if all you want is a single address. For instance, the code below works for single addresses:



<html>
<head>
<title>Address Lookup </title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript" src="http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6"></script>
<script type="text/javascript">

var map = null;
var findPlaceResults = null;
function GetMap()
{
map = new VEMap('myMap:newmap');
map.LoadMap();
map.Find(null,'100 Enterprise Way, Scotts Valley, CA 95066', null, null, null, null, true, true, null, true, GetCoordinates);
}

function GetCoordinates(layer, resultsArray, places, hasMore, veErrorMessage)
{
findPlaceResults = places[0].LatLong;
var myShape = new VEShape(VEShapeType.Pushpin, findPlaceResults);
myShape.SetTitle('CodeGear');
myShape.SetDescription('100 Enterprise Way<br>Scotts Valley, CA 95066<br>'+findPlaceResults.toString());
map.AddShape(myShape);
}

</script>
</head>
<body onload="GetMap();">
<div id='myMap:newmap' style="position:relative; width:800px; height:600px;"></div>
</body>
</html>


The problem showed up when I went to add the facilities to the map. Thinking it would all work the same way, I added the same Map.Find code for each facility in the state of interest and voila. Except the voila was, "How come I only get one pushpin?" not "See how great this works!" It turns out that part of the answer is the MS VEMap object works asynchronously. I could live with that if the map moved around a little as it added pushpins... well, at the very least, I could just hide the map until it was done.

Unfortunately, MS also made the calls non-reentrant. This is actually what they document the problem as. To me, the code IS non-reentrant in that it never calls itself. Apparently, our definitions of re-entrant differ significantly. To MS, non-reentrant means that you have to wait for the asynchronous call to complete before you can call it again. Of course, Javascript has little support for "waiting". I was able to create a work around using a do {} while (!completed); but it causes the browser to complain mightly, ties up a lot of CPU and is just generally ugly.

Seeing that this wasn't going to work very well, I went back to the drawing board. If I actually could provide the Lat/Lng for each object, the problem would go away. It turns out that there are not very many services that want to let you do this for free using a desktop tool. If you are willing to do a web mashup you can get away with quite a bit, but the traditional languages are left out in the cold for the most part.

This is where MapQuest comes in. They have a free service (you have to register, but that's it) that will let you have access to their mapping systems. Of course, they have no interface for Delphi, but they did publish their XML specifications so it's not too hard to write one. Keep in mind that you have to comply with the MapQuest TOS. The major one for me was that you can't use the Lat/Lng addresses with any other system than theirs. That means I can't use maps.live.com to produce my maps, I have to figure out how to use MapQuest. Considering my other choices was a complete web mashup rewrite or some commercial system that was going to cost a fortune, I think I'll live within those parameters.

I've only implemented the single interface I needed so far (GeoCoding), but the other interfaces are equally easy. This code is virtually identical (in the HTTP POST) as my DHL code. I've reposted the whole thing here for those needing a complete unit but will likely factor it out for my personal use.

Enjoy!

1. This is how the classes are used. I assume you recognize that the txtXXX are just text box fields and the resolver form has a single control on it that is a list box (other than the Select / Cancel buttons).



uses mapquest_api, w_AddyResolver;

procedure TForm1.btnGeoCodeClick(Sender: tobject);
var geocode:TMapQuestGeoCoder;
index, ix:integer;
begin
geocode:=TMapQuestGeoCoder.Create; //create the geocoder object
geocode.Password:='YOUR PASS'; //assign the password and client id you got from mapquest
geocode.ClientID:='YOUR ID'; //this will NOT work without it!!!

geocode.Address := edtAddress.Text; //assign the address info you want to code
geocode.City := edtCity.Text;
geocode.State := edtState.Text;
geocode.PostalCode := edtPostalCode.Text;
geocode.Country := edtCountry.Text;

if geocode.getFromMapQuest(true) then //this will send the info to mapquest and return the results
if geocode.ReturnedAddresses.Count>0 then //if we got a response
begin
if geoCode.ReturnedAddresses.Count=1 then //if there's only 1, no need to show a resolver
index:=0
else //we got multiple responses, the user will have to resolve which addy is correct
begin
index:=-1; //default to failure
for ix := 0 to geoCode.ReturnedAddresses.Count-1 do //for each addy
with geoCode.ReturnedAddresses.Address[ix] do
addyResolver.lb.Items.Add(Address+' '+City+' '+State+' '+Country); //load the listbox
if addyResolver.ShowModal=mrOk then //prompt the user, if they select one, mrOk will be set
index:=addyResolver.lb.ItemIndex; //get the addy
if index<0 then exit; //if the user failed to pick, get out
end;

lblLat.Caption:=geoCode.ReturnedAddresses.Address[index].Lat; //display the lat and long values
lblLong.Caption:=geoCode.ReturnedAddresses.Address[index].Long;
end;

end;


2. And this is the actual unit that does the work. BTW, the XML parser I'm using is OpenXML, a freely available, full source implementation that works for Delphi and Kylix.



unit mapquest_api;

interface

uses classes, contnrs;

type
//base class, you must derive descendants to make use of this class
//in order to successfully communicate with mapquest, you must attach YOUR ClientID, Password and APIKey
//you can register for free at mapquest.com
TMapQuestAPI = class
private
fClientID : string;
fPassword : string;
fAPIKey : string;
fURL : string;
protected
function getXML:string; virtual; abstract;
procedure parseResponse(response:TStringList); virtual; abstract;
public
constructor Create;
function getFromMapQuest(enableLogging:boolean):boolean;

property ClientID : string read fClientID write fClientID;
property Password : string read fPassword write fPassword;
property APIKey : string read fAPIKey write fAPIKey;
property URL : string read fURL;
end;

TReturnedAddyList=class;

//this descendant will implement the GeoCoder functionality from mapquest
//you must comply with mapquests terms of service, check mapquest.com to make sure
//that works for you
TMapQuestGeoCoder = class(TMapQuestAPI)
private
fAddress : string;
fState : string;
fCity : string;
fCountry : string;
fPostalCode: string;
fCounty : string;
fReturnedAddresses : TReturnedAddyList;
protected
function getXML:string; override;
procedure parseResponse(response:TStringList); override;
procedure ClearReturnedAddys;
public
constructor Create;
destructor Done;

property Address:string read fAddress write fAddress;
property City:string read fCity write fCity;
property State:string read fState write fState;
property Country:string read fCountry write fCountry;
property PostalCode:string read fPostalCode write fPostalCode;
property County:string read fCounty write fCounty;
property ReturnedAddresses:TReturnedAddyList read fReturnedAddresses;
end;

//container class for a returned address
TReturnedAddy = class
public
Address,
City,
State,
PostalCode,
Country,
County,
Lat,
Long : string;

constructor Create(aAddress, aCity, aState, aPostalCode, aCountry, aCounty, aLat, aLong:string);
end;

//you can get back MULTIPLE addresses if you send in something that is generic enough
//in that case, the COUNT will be > 1. Make sure you implement a resolver screen to handle this
TReturnedAddyList = class(TList)
protected
property Items;
property List;
public
function getAddy(index:integer):TReturnedAddy;
property Address[Index:Integer]:TReturnedAddy read getAddy;
end;


implementation

uses sysutils, dialogs, IdHttp, IdSSLOpenSSL, IdResourceStringsCore, IdLogFile, xdom_3_2;

const
CR = #13;
LF = #10;
EOL = CR+LF;
xmlEOL = EOL;

type
// this is a descendant of TidLogFile, it will create a plain text file with
// information about the transfer session
TlogFile = class(TidLogFile)
protected
procedure LogReceivedData(const AText, AData: string); override;
procedure LogSentData(const AText, AData: string); override;
procedure LogStatus(const AText: string); override;
public
procedure LogwriteString(const AText: string); override;
class function buildLogLine(data, prefix: string) : string;
end;

// this ensures the output of error and debug logs are in the same format, regardless of source
class function TlogFile.buildLogLine(data, prefix: string) : string;
begin

data := StringReplace(data, EOL, RSLogEOL, [rfReplaceAll]);
data := StringReplace(data, CR, RSLogCR, [rfReplaceAll]);
data := StringReplace(data, LF, RSLogLF, [rfReplaceAll]);

result := FormatDateTime('yy/mm/dd hh:nn:ss', now) + ' ';
if (prefix <> '') then
result := result + prefix + ' ';
result := result + data + EOL;
end;

// ---------------------------------------------------------------------------

procedure TlogFile.LogReceivedData(const AText, AData: string);
begin
// ignore AText as it contains the date/time
LogwriteString(buildLogLine(Adata, '<<'));
end;

// ---------------------------------------------------------------------------

procedure TlogFile.LogSentData(const AText, AData: string);
begin
// ignore AText as it contains the date/time
LogwriteString(buildLogLine(Adata, '>>'));
end;

// ---------------------------------------------------------------------------

procedure TlogFile.LogStatus(const AText: string);
begin
LogwriteString(buildLogLine(AText, '**'));
end;

// ---------------------------------------------------------------------------

procedure TlogFile.LogwriteString(const AText: string);
begin
// protected --> public
inherited;
end;

// ---------------------------------------------------------------------------

{ TMapQuestAPI }

//create the class, default everything to blank
constructor TMapQuestAPI.Create;
begin
fURL:='';
fClientID:='';
fPassword:='';
fAPIKey:='';
end;

// ---------------------------------------------------------------------------

//send the http post to mapquest and return a stringlist with the results (or nil if error)
function TMapQuestAPI.getFromMapQuest(enableLogging:boolean): boolean;
const logFileName = 'http.log';
var
plainData : TStringList;
HTTP : TidHTTP;
response : TStringList;
logFile : TLogFile;

//as mentioned in my article on http post for DHL, indy converts CR/LF pairs to &.
//This causes a problem when sending xml data.
//This routine will remove CR/LF pairs and replace them with a space
procedure ConvertCRLFtoSpace;
begin
if plainData.Count > 1 then begin
// break trailing CR&LF
plainData.Text := StringReplace(Trim(plainData.Text), sLineBreak, ' ',[rfReplaceAll]);
end else begin
plainData.Text := Trim(plainData.Text);
end;
end;

begin
result:=false; //by default, we get failure response in error
plainData := TStringList.Create; //init the container for xmiting the xml
try
plainData.Text:=getXML; //get the xml from the class and put it in the container
Http:=TidHTTP.Create(nil); //create the http class
try
HTTP.readTimeout := 10000; //setup the http timeouts and xmit types
HTTP.ConnectTimeout := 10000;
HTTP.Request.Contenttype := 'text/xml';
HTTP.HTTPOptions := [];
if enableLogging then //if we are going to log the events (useful if there's a problem)
begin
logFile := TLogFile.Create(nil); //create the log file
logFile.FileName:=logFileName; //set the default log file name - NOTE: you could get fancy with this, but I normally don't use it
logFile.Active:=true; //activate the log file (creates the file and intializes everything)
HTTP.Intercept := logFile; //set the logFile to the intercept of the HTTP to capture the events
end;
response:=TStringList.Create; //create the response container
try
ConvertCRLFtoSpace; //make sure you strip cr/lf pairs out or the xml will be malformed
response.Text := HTTP.Post(URL, plainData); //post the xml and receive the response xml
result:=true;
parseResponse(response);
finally
response.Free;
end;
finally
Http.free; //free the http class
end;
finally
plainData.Free; //free the container class
end;

end;

{ TMapQuestGeoCoder }

procedure TMapQuestGeoCoder.ClearReturnedAddys;
var ix:integer;
begin
for ix := 0 to fReturnedAddresses.Count-1 do
fReturnedAddresses.Address[ix].Free;
fReturnedAddresses.Clear;
end;

constructor TMapQuestGeoCoder.Create;
begin
fURL:='http://geocode.free.mapquest.com/mq/mqserver.dll?e=5&'; //this is the free server address. if you sign up for a different type of account, you'll want to set this to your account type
fReturnedAddresses:=TReturnedAddyList.Create;
end;

destructor TMapQuestGeoCoder.Done;
begin
ClearReturnedAddys;
fReturnedAddresses.Free;
end;

function TMapQuestGeoCoder.getXML: string; //returns the xml formated per the MapQuest API
const geocodeXML =
'<?xml version="1.0" encoding="ISO-8859-1"?>'+
'<Geocode version="1">'+

'<Address>'+
'<AdminArea1>%s</AdminArea1>'+
'<AdminArea3>%s</AdminArea3>'+
'<AdminArea5>%s</AdminArea5>'+
'<PostalCode>%s</PostalCode>'+
'<Street>%s</Street>'+
'</Address>'+

'<GeocodeOptionsCollection Count="0"/>'+

'<Authentication Version="2">'+
'<Password>%s</Password>'+
'<ClientId>%s</ClientId>'+
'</Authentication>'+
'</Geocode>';

begin
result := Format(geocodeXML, [Country,State,City,PostalCode,Address,Password,ClientID]); //make the request xml for geocding an address
end;

procedure TMapQuestGeoCoder.parseResponse(response: TStringList);
var
DOC:TXMLtoDomParser;
DOM:TDOMImplementation;
XML:TDOMDocument;
LocCollection:TDomNodeList;
Geo,
Node:TDomNode;

//you can't trust that the xml contains ALL element names so this wraps the testing conditions up
//to safely get the node values or a blank when not in the xml
function SafeGetNodeValue(node:TDomNode; name:string):string;
var DomElement:TDomElement;
begin
result:='';
DomElement:=Node.GetFirstChildElement(name);
if DomElement=nil then exit;
if DomElement.ChildNodes=nil then exit;
if DomElement.ChildNodes.Length=0 then exit;
result:=DomElement.ChildNodes.Item(0).NodeValue;
end;

//same as the previous but designed to get a subelement (LatLng/Lat or LatLng/Lng)
function SafeGetSubNodeValue(node:TDomNode; rootName, subName:string):string;
var RootElement,
SubElement:TDomElement;
begin
result:='';
RootElement:=Node.GetFirstChildElement(rootName);
if RootElement=nil then exit;
SubElement:=RootElement.GetFirstChildElement(subName);
if subElement=nil then exit;
if subElement.ChildNodes=nil then exit;
if subElement.ChildNodes.Length=0 then exit;
result:=subElement.ChildNodes.Item(0).NodeValue;
end;

begin
ClearReturnedAddys; //make sure there are no saved addys
DOC:=TXMLtoDomParser.Create(nil); //create the xml parser
DOM:=TDOMImplementation.Create(nil); //create the dom
try
DOC.DOMImpl:=DOM; //assign the dom to the xml parser
XML := DOC.StringtoDom(response.Text,'mapquest.xml',nil,true); //convert the response text to and xml document. NOTE: 'mapquest.xml' forces the correct creation of the node structures. If you pass blank, you get NO NODES
try
LocCollection := XML.GetElementsByTagName('LocationCollection'); //get the location collection (group of geoaddresses 1..n)
if LocCollection=nil then exit;

Node:=LocCollection.Item(0); //get the top level node
Geo:=Node.GetFirstChildElement('GeoAddress'); //get the first GeoAddress
while Geo<>nil do //while we have a GeoAddress
begin
fReturnedAddresses.Add(TReturnedAddy.Create(SafeGetNodeValue(Geo, 'Street'), //addy
SafeGetNodeValue(Geo, 'AdminArea5'), //city
SafeGetNodeValue(Geo, 'AdminArea3'), //state
SafeGetNodeValue(Geo, 'PostalCode'), //zip
SafeGetNodeValue(Geo, 'AdminArea1'), //country
SafeGetNodeValue(Geo, 'AdminArea4'), //county
SafeGetSubNodeValue(Geo, 'LatLng', 'Lat'), //Latitude
SafeGetSubNodeValue(Geo, 'LatLng', 'Lng') //Longitude
));
Geo:=Geo.NextSibling; //get the next GeoAddress
end;
finally
XML.Free; //free the XML document
end;
finally
DOM.Free; //free the dom
DOC.Free; //free the xml parser
end;
end;

{ TReturnedAddy }

//create the object and initialize the data
constructor TReturnedAddy.Create(aAddress, aCity, aState, aPostalCode, aCountry, aCounty, aLat, aLong: string);
begin
Address:=aAddress;
City:=aCity;
State:=aState;
PostalCode:=aPostalCode;
Country:=aCountry;
County:=aCounty;
Lat:=aLat;
Long:=aLong;
end;

{ TReturnedAddyList }

//easy way to get access to the accumulated returned addresses
function TReturnedAddyList.getAddy(index: integer): TReturnedAddy;
begin
result:=TReturnedAddy(Items[index]);
end;

end.

Comments

Anonymous said…
A function being reentrant meaning it can be called safely again while it is executing and has not returned. A function that calls itself directly or indirectly is recursive. You mixed up the terms.
Marshall Fryman said…
You are, of course, correct. Thanks.

Popular posts from this blog

SMTP Mail and Indy (again)

Having spent a lot of time recently working on a ping scanner using Indy, I noticed that there's a lot of questions still on using SMTP with Indy. Let me first say that I am not an Indy expert. I get along with it successfully but find I still have to research things frequently. With the disclaimer out of the way, we can get to the offering. A while back I wrote a handy little unit to simply send a mail message with or without attachments and with or without providing authentication. It even has support for OpenSSL. I use this unit in a number of applications and have seen it successfully send hundreds of e-mails without issue. I recently added support for threaded message sending to take advantage of the multi-core system I'm now running on. This code has had a few additions (like the logger) that I've gleaned over time from various newsgroup postings, but I didn't record the authors so let me credit the anonymous Internet authors who support Indy. It's really amaz...

Detecting a virtualized environment

CubicDesign on delphi-talk.elists.org recently asked the question: "How do I know/detect if my software is running under Windows [or a virtual environment]?" Well, it turns out that it's a lot harder to tell than you would think. Apparently, the VM (VMware, Xen, Wine, etc.) doesn't really want you to be able to do this. At least not easily. For VMware, there is a decent little C routine called jerry.c that does the trick. Jerry actually uses a simple communication channel that VMware left open. It's not 100% foolproof since the admin can change the channel, but that's not likely going to happen unless the system is specifically designed to be a honeypot. If you're running on a honeypot and still have a legitimate reason for detection, you could look at the much more complex scoopy implementation which inspects how the system is actually virtualized using the SIDT CPU instruction instead of a communication channel. Another reference (red pill) is here . F...

Delphi Case with Strings

Zarko Gajic posted about this topic on his Delphi Tips recently showing how one could use a case statement with strings. His solution basically passes in an array on the stack and then iterates through it providing the index number of the matching string. I don't often want to do this, but the idea comes up occassionally enough that I thought I'd play with it a little. The first thing that struck me with this is that passing things on the stack is bound to be slow. Any time you can avoid memory allocation in your routines, do it. The way Zarko wrote his StringToCaseSelect routine created a COPY of the information on the stack. In my testing, just changing the CaseList: array of string to CaseList:const array of string improved the performance of the code by almost 30% for his example. Mind, I'm not using hyper-precise counters; however, it definitely makes a difference. Secondly, I was curious how the performance changed if I used the old stand-by: If then else. Using the ...

Copyright 2008-2022, Marshall Fryman