My company is looking for a few good developers for some Delphi contract work. Are you (or someone you know) looking for a bit of work and fit one of the profiles below? If so, drop me a line at marshall dot fryman at gmail dot com. I'm looking for experienced Delphi developers only. You'll need references and samples. We only speak English so you will need to be reasonably conversant with it.
Digital Metaphors ReportBuilder specialist - We need over a hundred reports written. The reports are financially-oriented. We'll be doing Balance Sheet, Sales Summaries, Reorder Reports, etc. The focus here is mostly on the report writing and less on configuring the underlying RB engine. You'll need to be familiar with GL systems and accounting reports.
RemObjects specialist - I need someone who is very familiar with RemObjects SDK. We have the licenses but have run out of resources in the short-term so we're looking to contract a new 3-tier system based on the SDK. In our case, we will have a client, local server, and remote server (remote read as "over the Internet"). We will transfer data, configuration information and command-and-control requests over this conduit. We will also utilize Hydra on the local server to provide for plug-in extensions.
Accounting Integration - I am looking for an accounting developer who has experience in working with both the Sage SDK and the QB SDK to transfer accounting data from an external application to either Sage or QB products. We already have a full GL, we just need to push it to the appropriate accounting target.
Thanks in advance for any and all interest.
Clarification:
I have offices in Clearwater, FL and Anaheim, CA. There is no pressing need for you to be located near either of these sites. Remote workers are fine. My concern is the quality of the work not the location of the worker. I should also be clear I'm looking for contract workers at present. I may hire developers permanently mid-next year and would certainly discuss any opportunities with people I've contracted with before doing a general search.
Tuesday, November 10, 2009
Friday, July 31, 2009
D2007 and large text files
Has anyone noticed that Delphi 2007 cannot append to a text file over 2GB in size? I freely admit that I had a problem in my code that created log files that large in the first place... but I also don't expect my compiler to blow up when dealing with files of any size.
My first warning was when the append command started throwing I/O Error 131 messages. Error 131 is negative seek. The only way that could be is if CG has a signed int32 it's using for the filesize. When I hit the upper bounds of int32, it just wraps around to a negative number.
In my case, I just fixed the bug and stopped making huge text files. Otherwise, it looks like I'd have to change how the append function works in the System library. Does anyone know if D2009 also has this problem?
My first warning was when the append command started throwing I/O Error 131 messages. Error 131 is negative seek. The only way that could be is if CG has a signed int32 it's using for the filesize. When I hit the upper bounds of int32, it just wraps around to a negative number.
In my case, I just fixed the bug and stopped making huge text files. Otherwise, it looks like I'd have to change how the append function works in the System library. Does anyone know if D2009 also has this problem?
Thursday, July 30, 2009
Gmail, POP3 and Indy
In my ever expanding need for dealing with the outside world, I've discovered a quirky little thing about Indy 10. When using the TIdPOP3 component with Gmail, you have to manually call DisconnectNotifyPeer or delete commands are ignored. I suppose this isn't quirky as much as not really documented any place I was able to find. Let's backup and start at the beginning, shall we?
I have a Windows service that polls a POP3 account looking for messages that it can import into a DB. It has all sorts of fun rules that can be assigned to discover what needs to be imported, what database it goes into, etc. At any rate, this code has been merrily chugging away on both Yahoo and SmarterMail servers with nary a hiccup. I recently needed to watch a Google domain app e-mail address and didn't think much of it.
Of course, in order to do so you have to implement SSL for POP3. As I've posted in the past, this is not really much of an issue, you simply create the TIdSSLIOHandlerSocketOpenSSL object and assigned it to the IOHandler like so:
Anyway, all was proceeding swimmingly until I retrieved the first message. In my case, I automatically delete any e-mail that matches my rules so it will not be imported a second time. This works fine with Yahoo and SmarterMail. You just call DeleteMessage(msgNumber) and voila, away it goes. With Gmail, no such luck.
Gmail actually has a three different settings under POP3. One keeps messages in the inbox after reading via POP, one archives it and one deletes it. Thinking this option was set wrong, I tried all three options... repeatedly... still no luck. I tried searching pretty extensively on this one and finally found the clue with RFC 1939. Gmail honors the "you must notify me you are quitting or I will not delete anything" rule that 1939 dictates.
After checking the code for the IdPOP3, it turns out calling Free does not issue the QUIT command. It makes sense for it not to do so. What if you wanted to avoid the UPDATE state? It was just surprising that some mail servers worked without this command and some required it.
At any rate, I hope this helps someone else who's working with POP3 commands.
I have a Windows service that polls a POP3 account looking for messages that it can import into a DB. It has all sorts of fun rules that can be assigned to discover what needs to be imported, what database it goes into, etc. At any rate, this code has been merrily chugging away on both Yahoo and SmarterMail servers with nary a hiccup. I recently needed to watch a Google domain app e-mail address and didn't think much of it.
Of course, in order to do so you have to implement SSL for POP3. As I've posted in the past, this is not really much of an issue, you simply create the TIdSSLIOHandlerSocketOpenSSL object and assigned it to the IOHandler like so:
Pop3Srv.IOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(Pop3Srv);
Pop3Srv.UseTLS := utUseImplicitTLS;
Anyway, all was proceeding swimmingly until I retrieved the first message. In my case, I automatically delete any e-mail that matches my rules so it will not be imported a second time. This works fine with Yahoo and SmarterMail. You just call DeleteMessage(msgNumber) and voila, away it goes. With Gmail, no such luck.
Gmail actually has a three different settings under POP3. One keeps messages in the inbox after reading via POP, one archives it and one deletes it. Thinking this option was set wrong, I tried all three options... repeatedly... still no luck. I tried searching pretty extensively on this one and finally found the clue with RFC 1939. Gmail honors the "you must notify me you are quitting or I will not delete anything" rule that 1939 dictates.
When the client issues the QUIT command from the TRANSACTION state, the POP3 session enters the UPDATE state. (Note that if the client issues the QUIT command from the AUTHORIZATION state, the POP3 session terminates but does NOT enter the UPDATE state.) If a session terminates for some reason other than a client-issued QUIT command, the POP3 session does NOT enter the UPDATE state and MUST not remove any messages from the maildrop.
After checking the code for the IdPOP3, it turns out calling Free does not issue the QUIT command. It makes sense for it not to do so. What if you wanted to avoid the UPDATE state? It was just surprising that some mail servers worked without this command and some required it.
At any rate, I hope this helps someone else who's working with POP3 commands.
Friday, April 17, 2009
RAID, files and cloud storage
RAID is a method that takes independent drives and lets a system group them together for security (redundancy or parity), speed enhancements, storage space increases or all three. One of the long-time stalwarts of the RAID environment is RAID 5. In RAID 5, you need at least 3 identically sized disks. They are combined so that the storage space is N-1 (i.e., in a 3-drive system, total space is 2x drive size). The last disk is used for parity. With a parity drive, you can remove any one of the drives and still have access to your data. If you remove two or more of the drives though, you'd better have a good backup.
How does this work? Through the magic of XOR. The following statements are all true:
That's how parity lets you lose one disk and still recover your data. The same rules also apply in larger sets. Notice that you have to rotate the position of the parity data though. So a 5 drive system looks like:
The other interesting thing to note is that the size stored on each drive is 1/(n-1) of the total file size. Thus a 100 byte file in a RAID 5 system only stores 20 bytes on each drive. This is where the space increase comes from.
Now, the question is, why do we care? Other than it's nice to know how something works, this technique could be applied to cloud storage. If you follow some of the events that have happened with online storage providers, you may have seen a number of them come and go. The problem is, if they disappear or lose your data, what happens then?
If you are using them as a convenient off-site data storage pool for large amounts of infrequently used data, you could implement a RAID 5-style data split. Keep in mind there's a bit of overhead in doing so, but the point of this exercise is to reduce your dependence on any one provider. Coincidentally, it will likely cost you the same or less than using a single cloud storage provider.
Take for instance Amazon's S3 service and Rackspace's Mosso Cloud Files. Both services charge per gigabyte per month. For arguments sake, let's assume we have a 10GB RAR file (BIGFILE.RAR) that we want to backup. If we split it into a RAID 5, 3 drive format, that would leave us with BIGFILE.RAR.1, BIGFILE.RAR.2 and BIGFILE.RAR.RAID. Each file will be 5GB (10GB / (3-1)) in size. I can upload one file to Amazon, one file to Rackspace and retain one file on my hard drive. I've now backed up the file in an online form that can be recovered even if I lose my hard drive or Amazon / Rackspace has an outage at the same moment I need to have access to my data. So long as I have access to any two parts of the file, I can create the original output file.
Obviously, Amazon and Rackspace are large enough it is unlikely they'd actually lose the data over a longer period of time. The same can't be said of some the smaller players in the market. Companies like Streamload managed to wipe out about half of their customers data during a reorganization before finally closing their doors. Anyone caught unaware lost all of their data.
I should also note that you could use a PAR or PAR2 system. The easiest to use implementation is probably QuickPAR. It uses a similar system to what I've shown here but the source is not very conducive to Delphi developers. From what I can tell, it was originally developed to push binary files around Usenet, but it would work equally well for cloud storage. If you're just looking for a good off-the-shelf tool, QuickPAR is probably the way to go. If you're interested in developing a parity solution that you can embed in your code, the source is included below.
How does this work? Through the magic of XOR. The following statements are all true:
A XOR B = PAR
PAR XOR B = A
A XOR PAR = B
That's how parity lets you lose one disk and still recover your data. The same rules also apply in larger sets. Notice that you have to rotate the position of the parity data though. So a 5 drive system looks like:
A XOR B XOR C XOR D XOR E = PAR
PAR XOR B XOR C XOR D XOR E = A
A XOR PAR XOR C XOR D XOR E = B
A XOR B XOR PAR XOR D XOR E = C
A XOR B XOR C XOR PAR XOR E = D
A XOR B XOR C XOR D XOR PAR = E
The other interesting thing to note is that the size stored on each drive is 1/(n-1) of the total file size. Thus a 100 byte file in a RAID 5 system only stores 20 bytes on each drive. This is where the space increase comes from.
Now, the question is, why do we care? Other than it's nice to know how something works, this technique could be applied to cloud storage. If you follow some of the events that have happened with online storage providers, you may have seen a number of them come and go. The problem is, if they disappear or lose your data, what happens then?
If you are using them as a convenient off-site data storage pool for large amounts of infrequently used data, you could implement a RAID 5-style data split. Keep in mind there's a bit of overhead in doing so, but the point of this exercise is to reduce your dependence on any one provider. Coincidentally, it will likely cost you the same or less than using a single cloud storage provider.
Take for instance Amazon's S3 service and Rackspace's Mosso Cloud Files. Both services charge per gigabyte per month. For arguments sake, let's assume we have a 10GB RAR file (BIGFILE.RAR) that we want to backup. If we split it into a RAID 5, 3 drive format, that would leave us with BIGFILE.RAR.1, BIGFILE.RAR.2 and BIGFILE.RAR.RAID. Each file will be 5GB (10GB / (3-1)) in size. I can upload one file to Amazon, one file to Rackspace and retain one file on my hard drive. I've now backed up the file in an online form that can be recovered even if I lose my hard drive or Amazon / Rackspace has an outage at the same moment I need to have access to my data. So long as I have access to any two parts of the file, I can create the original output file.
Obviously, Amazon and Rackspace are large enough it is unlikely they'd actually lose the data over a longer period of time. The same can't be said of some the smaller players in the market. Companies like Streamload managed to wipe out about half of their customers data during a reorganization before finally closing their doors. Anyone caught unaware lost all of their data.
I should also note that you could use a PAR or PAR2 system. The easiest to use implementation is probably QuickPAR. It uses a similar system to what I've shown here but the source is not very conducive to Delphi developers. From what I can tell, it was originally developed to push binary files around Usenet, but it would work equally well for cloud storage. If you're just looking for a good off-the-shelf tool, QuickPAR is probably the way to go. If you're interested in developing a parity solution that you can embed in your code, the source is included below.
procedure File2RaidFiles(fileName:string; raidLength:integer);
var FS:TFileStream;
outputFS:array of TFileStream;
byteArray:array of byte;
ix:integer;
begin
//test to make sure we were called correctly
if not FileExists(fileName) then
raise Exception.Create(Format('File %s doesn''t exist', [fileName]));
if raidLength<2 then
raise Exception.Create(Format('Raid Length must be greater than 1. Given value was %d',[raidLength]));
FS:=TFileStream.Create(fileName,fmOpenRead);
try
setLength(outputFS, raidLength+1); //+1 for the parity byte
setLength(byteArray, raidLength+1);
for ix := 0 to raidLength do //create an output location for each stripe (named .stripe#) and the parity file (named .raid)
begin
if ix=raidLength then
outputFS[ix]:=TFileStream.Create(fileName+'.raid',fmCreate)
else
outputFS[ix]:=TFileStream.Create(fileName+'.'+IntToStr(ix),fmCreate);
end;
try
while FS.Position<FS.Size do //while we haven't hit the end of the file
begin
FS.Read(byteArray[0],raidLength); //read in the bytes to the byteArray
for ix := 0 to raidLength-2 do //this calcs the parity byte, it's calculated by XORing the other bytes to it
byteArray[raidLength]:=byteArray[ix] xor byteArray[ix+1];
for ix := 0 to raidLength do //write out the bytes to the respective stripes (stripes are < raidlength) and the parity file (outputFS[raidlength])
outputFS[ix].Write(byteArray[ix],1);
end;
finally
for ix := 0 to raidLength do //clean up the output streams
outputFS[ix].Free;
end;
finally
FS.Free; //clean up the input stream
end;
end;
procedure RaidFiles2File(fileName, outputName:string; raidLength:integer);
var FS:TFileStream;
inputFS:array of TFileStream;
testFS:TFileStream;
byteArray:array of byte;
ix, damage:integer;
countOfMissing:integer;
checkByte:byte;
checkFails:boolean;
begin
//test to make sure we were called correctly
if FileExists(outputName) then
raise Exception.Create(Format('File %s already exist', [outputName]));
if raidLength<2 then
raise Exception.Create(Format('Raid Length must be greater than 1. Given value was %d',[raidLength]));
//init the basics
checkFails:=false;
testFS:=nil;
countOfMissing:=0;
damage:=0;
setLength(inputFS, raidLength+1); //+1 is the parity byte
setLength(byteArray, raidLength+1);
for ix := 0 to raidLength do
inputFS[ix]:=nil;
//setup the output file stream
FS:=TFileStream.Create(outputName,fmCreate);
try
//create the input file streams. make sure we pickup the count of missing streams and a pntr to an input stream for use later on (any stream will do, just testing for eof
for ix := 0 to raidLength do
begin
if ix=raidLength then //this is the parity stream
begin
if FileExists(fileName+'.raid') then
inputFS[ix]:=TFileStream.Create(fileName+'.raid',fmOpenRead)
end
else //this is a stripe stream
begin
if FileExists(fileName+'.'+IntToStr(ix)) then
begin
inputFS[ix]:=TFileStream.Create(fileName+'.'+IntToStr(ix),fmOpenRead);
testFS:=inputFS[ix]; //this is just to test for eof. all files are the same size
end;
end;
if inputFS[ix]=nil then inc(countOfMissing); //if we didn't get an input stream, add to the missing count
end;
//you are only allowed to have 1 missing input file
if countOfMissing>1 then
raise Exception.Create('Unable to recover file! You must have at least N-1 parts of the file');
assert(testFS<>nil, 'testFS=nil? This should never happen');
while testFS.Position<testFS.Size do //while we are not at the end of the input file
begin
if countOfMissing=0 then //if there are no missing streams, we can just remerge the data together
begin
for ix := 0 to raidLength do
inputFS[ix].Read(byteArray[ix],1);
checkByte:=0;
//calc a checkByte to make sure it all still agrees
for ix := 0 to raidLength-2 do //0 based, stop 1 short of the end
if ix=0 then
checkByte:=byteArray[ix] xor byteArray[ix+1] //seed check byte by XORing the 1st two bytes together
else
checkByte:=checkbyte xor byteArray[ix+1]; //XOR the bytes together
if checkByte<>byteArray[raidLength] then //the new checkByte doesn't match the old parity byte. That means we have a problem, the file doesn't match
checkFails:=true;
end
else //if there are missing streams, we have to calculate the missing data
begin //you have to reverse the XOR with the parity bit to determine the result
for ix := 0 to raidLength-1 do //not counting the end of the array, that's where we'll store the damaged byte
if inputFS[ix]<>nil then //if this is a valid stream, read it's data
inputFS[ix].Read(byteArray[ix],1)
else
begin //this isn't a valid stream, so remember this is damaged and read from the parity stream for this position
damage:=ix;
inputFS[raidLength].Read(byteArray[ix],1);
end;
//this is calcs the damaged byte into the end of the array (raidLength)
for ix := 0 to raidLength-2 do //0 based, stop 1 short of the end
byteArray[raidLength]:=byteArray[ix] xor byteArray[ix+1]; //XOR the bytes together
byteArray[damage]:=byteArray[raidLength]; //replace the damaged byte with the restored byte
end;
FS.Write(byteArray[0],raidLength); //write out the merged (and psbly restored) data
end;
finally
for ix := 0 to raidLength do //clean up the memory from the input streams
if inputFS[ix]<>nil then inputFS[ix].Free;
FS.Free; //clean up the memory for the output stream
end;
if checkFails then
raise Exception.Create(Format('This file (%s) does not match to its parity checks. Damage may be present', [outputName]));
end;
Wednesday, January 14, 2009
Qt becomes LGPL licensed
Nokia has announced that the new Qt library (4.5) will be available under the LGPL license (March 2009). Hopefully, this will mean that Embarcadero will revive Linux support with it's upcoming 64-bit, compile-to-many-targets compiler rewrite.
Actually, it could mean that we would finally have a true CLX revision that would let us have native write-once, compile-to-many-targets, cross platform support for Windows, Mac, Linux, Embedded Linux, Windows CE, and the S60 (eventually). If Delphi's component architecture could be revamped to produce true Qt widgets, Embarcadero would suddenly enable all of the Delphi component vendors to gain access to a client-base that is much larger than what Delphi currently offers. Qt does not have much selection from third-party component vendors. A DevExpress or TMS Software offering would provide a nice upgrade to the basic Qt widgets.
Delphi Prism theoretically supports multi-platform through the use of Mono but you have to add the managed code overhead to everything. There was a recent article on using Mono to develop for the iPhone which talked about stripping the managed code down to a true compiled state (Apple doesn't permit managed code to run apparently.) It was done with C# but there is no particular reason (that I'm aware of) Delphi Prism couldn't work the same way. Has anyone tried to do multi-platform with Prism?
Actually, it could mean that we would finally have a true CLX revision that would let us have native write-once, compile-to-many-targets, cross platform support for Windows, Mac, Linux, Embedded Linux, Windows CE, and the S60 (eventually). If Delphi's component architecture could be revamped to produce true Qt widgets, Embarcadero would suddenly enable all of the Delphi component vendors to gain access to a client-base that is much larger than what Delphi currently offers. Qt does not have much selection from third-party component vendors. A DevExpress or TMS Software offering would provide a nice upgrade to the basic Qt widgets.
Delphi Prism theoretically supports multi-platform through the use of Mono but you have to add the managed code overhead to everything. There was a recent article on using Mono to develop for the iPhone which talked about stripping the managed code down to a true compiled state (Apple doesn't permit managed code to run apparently.) It was done with C# but there is no particular reason (that I'm aware of) Delphi Prism couldn't work the same way. Has anyone tried to do multi-platform with Prism?
Monday, November 3, 2008
Word, WordPad and RTF
In yet another stunning victory for Microsoft's cross-compatibility, their RTF system is incompatible between their own products. Let me begin by explaining that I have a Delphi app that uses a mail merge system to merge database text into a document. Naturally, you can do this a lot of different ways but, for my particular instance, I need to merge from a non-ODBC compliant database and then automatically E-mail the resulting document to the correct person. This is a management tool we use fairly heavily.
In the past, I've always just used raw HTML formatting because it's handy and relatively standard. The particular request I've been working on is to format the resulting merge so that it can be printed in a "one summary page per item" format. HTML has the ability to do this with a style sheet as such:
and use it via:
Thanks to HTML Goodies for the information.
Now, the downside to this is that HTML print formatting appears to be considered optional by... well... everyone. I initially sent the HTML to my Gmail account. It appears fine but without the page breaks. I would expect this considering Gmail has it's own HTML enclosure. I would not expect the PRINT to ignore all of the HTML formatting, but it does. I suppose this makes some sense in light of the header information they put at the top of the output. The process of placing the header must remove the rest of the print formatting. Keep in mind this is the equivalent of an HTML E-mail, not an HTML attachment. When I converted over to an HTML attachment, Gmail dutifully opens the attachment and FF3 prints the document out just fine.
On to Outlook. One would think that Outlook should honor HTML formatting since, unlike Gmail, it doesn't actually use any HTML for display or printing. This is not so. HTML prints from Outlook without the page breaks from the normal reader pane. Thinking I could force the issue, I dbl-clicked the message in Outlook (which of course puts it in Word) and voila - no difference. This is turning into a major hassle. Additionally, because Outlook is so clever, it won't really let you send an HTML attachment. Instead, it displays it in-line with the same problems printing as with an HTML E-mail.
On to the solution...
I had to select some format other than HTML but it must be 7-bit text-based because of the way my Delphi merge app works. That pretty much leaves me with PDF or RTF as easy standards that everyone can open. I settled on RTF because it's easy to create an RTF file in OpenOffice, save it out and hack it apart with TSE Pro. My merge codes are just enclosed in << >> (ala MS Word style) so a name would be
This is not to say that Word doesn't open the document. It does, it just ignores the page breaks. I tried the same RTF file in Word 07 and Word 03 with the same results. Even more annoying, if I open the file in WordPad and then save it back out as RTF, Word still won't have the page breaks. After reading the RTF documentation, I was left with the impression that \page should always generate a new page. It turns out that Word won't take just a \page like the documentation, WordPad and OpenOffice. It HAS to be "\par \page \par " and please don't forget the ending space. In Word, this works fine. In WordPad and OO, it generates a leading blank line. In my case, I can call this close enough sine the summary pages are so short anyway. If I was pressed for space though, it would be an issue that would probably force me to PDF.
In another quirky little thing, when I created my file in OO and saved it out as RTF, my file size was 38k. If I open the file in WordPad and save it out, it shrinks to 13k. If I open the original in Word 07 and save it out, it grows to 87k. How can the same RTF encoding size vary that much from the same company? Even more annoying, why aren't WordPad and Word interoperable using RTF?
In the past, I've always just used raw HTML formatting because it's handy and relatively standard. The particular request I've been working on is to format the resulting merge so that it can be printed in a "one summary page per item" format. HTML has the ability to do this with a style sheet as such:
<STYLE TYPE="text/css">
P.breakhere {page-break-before: always}
</STYLE>
and use it via:
<p class="breakhere">
Thanks to HTML Goodies for the information.
Now, the downside to this is that HTML print formatting appears to be considered optional by... well... everyone. I initially sent the HTML to my Gmail account. It appears fine but without the page breaks. I would expect this considering Gmail has it's own HTML enclosure. I would not expect the PRINT to ignore all of the HTML formatting, but it does. I suppose this makes some sense in light of the header information they put at the top of the output. The process of placing the header must remove the rest of the print formatting. Keep in mind this is the equivalent of an HTML E-mail, not an HTML attachment. When I converted over to an HTML attachment, Gmail dutifully opens the attachment and FF3 prints the document out just fine.
On to Outlook. One would think that Outlook should honor HTML formatting since, unlike Gmail, it doesn't actually use any HTML for display or printing. This is not so. HTML prints from Outlook without the page breaks from the normal reader pane. Thinking I could force the issue, I dbl-clicked the message in Outlook (which of course puts it in Word) and voila - no difference. This is turning into a major hassle. Additionally, because Outlook is so clever, it won't really let you send an HTML attachment. Instead, it displays it in-line with the same problems printing as with an HTML E-mail.
On to the solution...
I had to select some format other than HTML but it must be 7-bit text-based because of the way my Delphi merge app works. That pretty much leaves me with PDF or RTF as easy standards that everyone can open. I settled on RTF because it's easy to create an RTF file in OpenOffice, save it out and hack it apart with TSE Pro. My merge codes are just enclosed in << >> (ala MS Word style) so a name would be
. Anyway, this all works, although I don't really recommend reading RTF as a form of entertainment. Many trial and error attempts later I had a document that works fine in WordPad and OpenOffice. It just won't work in Word.
<<name>>
This is not to say that Word doesn't open the document. It does, it just ignores the page breaks. I tried the same RTF file in Word 07 and Word 03 with the same results. Even more annoying, if I open the file in WordPad and then save it back out as RTF, Word still won't have the page breaks. After reading the RTF documentation, I was left with the impression that \page should always generate a new page. It turns out that Word won't take just a \page like the documentation, WordPad and OpenOffice. It HAS to be "\par \page \par " and please don't forget the ending space. In Word, this works fine. In WordPad and OO, it generates a leading blank line. In my case, I can call this close enough sine the summary pages are so short anyway. If I was pressed for space though, it would be an issue that would probably force me to PDF.
In another quirky little thing, when I created my file in OO and saved it out as RTF, my file size was 38k. If I open the file in WordPad and save it out, it shrinks to 13k. If I open the original in Word 07 and save it out, it grows to 87k. How can the same RTF encoding size vary that much from the same company? Even more annoying, why aren't WordPad and Word interoperable using RTF?
Friday, August 15, 2008
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:
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).
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.
<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.
Subscribe to:
Posts (Atom)