<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>John Bafford &#187; php</title>
	<atom:link href="http://bafford.com/tag/php/feed/" rel="self" type="application/rss+xml" />
	<link>http://bafford.com</link>
	<description>coding in purple</description>
	<lastBuildDate>Sat, 03 Oct 2009 19:57:26 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.0</generator>
		<item>
		<title>Twitterslurp open source release</title>
		<link>http://bafford.com/2009/06/30/twitterslurp-open-source-release/</link>
		<comments>http://bafford.com/2009/06/30/twitterslurp-open-source-release/#comments</comments>
		<pubDate>Tue, 30 Jun 2009 22:44:35 +0000</pubDate>
		<dc:creator>John Bafford</dc:creator>
				<category><![CDATA[Software]]></category>
		<category><![CDATA[javascript]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[open source]]></category>
		<category><![CDATA[twitter]]></category>
		<category><![CDATA[twitterslurp]]></category>

		<guid isPermaLink="false">http://bafford.com/?p=346</guid>
		<description><![CDATA[Last month, I wrote about Twitterslurp, the twitter searching tool I developed at The Bivings Group, which displays a constantly-updating stream of tweets, as well as a leaderboard and stats graphs. Today, we are very happy to release it as open source. You can download Twitterslurp from its Google Code project page at http://twitterslurp.googlecode.com/. Since [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://bafford.com/2009/05/20/tracking-phptek-tweets-with-twitterslurp">Last month</a>, I wrote about Twitterslurp, the twitter searching tool I developed at <a href="http://www.bivings.com/">The Bivings Group</a>, which displays a constantly-updating stream of tweets, as well as a leaderboard and stats graphs.</p>
<p>Today, we are very happy to <a href="http://www.bivingsreport.com/2009/the-bivings-group-releases-twitterslurp-to-open-source-community/">release it as open source</a>. You can download Twitterslurp from its Google Code project page at <a href="http://twitterslurp.googlecode.com/"> http://twitterslurp.googlecode.com/</a>.</p>
<p>Since last month, I&#8217;ve made a lot of changes to improve the quality (and ease of configuration) of the Twitterslurp code. Twitterslurp&#8217;s error handling has been improved, and I added the ability to start and stop the tweet stream and show more than the most recent 20 tweets. Our graphics team also created a spiffy logo.</p>
<p>Yesterday and today, Twitterslurp has been driving a video wall of tweets at the <a href="http://www.personaldemocracy.com/twitter/">Personal Democracy Forum</a> conference in NYC. The conference, which just ended, had over 17,000 tweets in the last two days.</p>
<p>Previously, we ran test versions of Twitterslurp during mysqlconf and <a href="http://twitter.bivings.com/tek09/">php|tek</a>, and officially on behalf of the <a href="http://twitter.bivings.com/dpc/">Dutch PHP Conference</a>. Twitterslurp started as a project for a client to allow them to track tweets, and give members of their website rewards for tweeting with a particular hashtag.</p>
<p>We&#8217;ve also set up a copy of Twitterslurp <a href="http://twitter.bivings.com/twitterslurp/">tracking itself</a>.</p>
<p>We&#8217;d love for you to check out Twitterslurp, and we&#8217;re open to any and all feedback.</p>
]]></content:encoded>
			<wfw:commentRss>http://bafford.com/2009/06/30/twitterslurp-open-source-release/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Tracking php&#124;tek Tweets With Twitterslurp</title>
		<link>http://bafford.com/2009/05/20/tracking-phptek-tweets-with-twitterslurp/</link>
		<comments>http://bafford.com/2009/05/20/tracking-phptek-tweets-with-twitterslurp/#comments</comments>
		<pubDate>Wed, 20 May 2009 17:03:02 +0000</pubDate>
		<dc:creator>John Bafford</dc:creator>
				<category><![CDATA[javascript]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[ajax]]></category>
		<category><![CDATA[apc]]></category>
		<category><![CDATA[flot]]></category>
		<category><![CDATA[jquery]]></category>
		<category><![CDATA[phptek]]></category>
		<category><![CDATA[tek09]]></category>
		<category><![CDATA[twitter]]></category>
		<category><![CDATA[twitterslurp]]></category>

		<guid isPermaLink="false">http://bafford.com/?p=327</guid>
		<description><![CDATA[For a client at work a few months ago, I created a Twitter search tool, Twitterslurp, that put all the tweets related to the client&#8217;s project on their webpage, updated in (close to) real-time via AJAX. I&#8217;ve since added a lot of features, including a set of graphs, and we&#8217;ve set up a version of [...]]]></description>
			<content:encoded><![CDATA[<p>For a client at <a href="http://bivings.com/">work</a> a few months ago, I created a <a href="http://twitter.com/">Twitter</a> search tool, Twitterslurp, that put all the tweets related to the client&#8217;s project on their webpage, updated in (close to) real-time via AJAX.</p>
<p>I&#8217;ve since added a lot of features, including a set of graphs, and we&#8217;ve set up a version of Twitterslurp for <a href="http://twitter.bivings.com/">php|tek 2009</a>.<br />
<span id="more-327"></span></p>
<p>Twitterslurp consists of two parts. The first is a PHP cron job that queries Twitter for new tweets matching a search string and adds them to a database. The second is JavaScript code that interfaces with a PHP script that searches the database for new tweets. In a nutshell, the JavaScript examines the web page for placeholders that represent different types of data (tweets, the leaderboard, and the graphs), and then posts to the PHP script with a JSON object detailing the data it wants. Once PHP responds, also with a JSON object, the JavaScript takes that data, formats it, and updates the page with the new content. The web page updates automatically based on an interval specified in the response object, so in the unlikely event the server came under heavy load, we would be able to easily throttle requests.</p>
<p>The new feature I&#8217;m really happy with is the <a href="http://twitter.bivings.com/stats.html">stats page</a> I&#8217;ve added, using <a href="http://jquery.com/">jQuery</a> and a graphs library called <a href="http://flot.googlecode.com/">flot</a>.</p>
<p>Flot uses the canvas tag introduced in Safari, and later supported by Firefox and Opera (and Chrome), to draw 2D images (graphs, in this case) within the browser. IE is supported with a library that implements canvas support on top of VML.</p>
<p>Flot is pretty simple to learn and use, thanks to its detailed API documentation and helpful set of examples. It actually has relatively few API functions. For the most part, you implement graphs simply by building a JavaScript object with the graph data and configuration options you want, and tell flot where to put the graph. (Twitterslurp builds this object in PHP and gives it to the JavaScript code via a JSON object.) With a few hours of work, I was able to put together the four graphs shown on the stats page, all of which have a rollover that shows the value of individual data points.</p>
<p>To lessen the server load, the stats page updates less frequently, and the graph data is cached on the server via APC, so the graph data doesn&#8217;t have to be regenerated on every request.</p>
<p>Please feel free to <a href="http://twitter.bivings.com/">take a look</a>, and let me know if you have any comments or suggestions, either here, or via Twitter (<a href="http://twitter.com/jbafford">@jbafford</a>).</p>
<p>We&#8217;re planning on taking what we&#8217;ve learned with Twitterslurp for php|tek, clean up the code (which wasn&#8217;t intended on providing graph data), adding a few more new features, and releasing it with an open source license in June. Stay tuned for more details&#8230;</p>
]]></content:encoded>
			<wfw:commentRss>http://bafford.com/2009/05/20/tracking-phptek-tweets-with-twitterslurp/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Pickens Plan Scaling Talk Slides Are Available</title>
		<link>http://bafford.com/2009/05/15/pickens-plan-scaling-talk-slides-are-available/</link>
		<comments>http://bafford.com/2009/05/15/pickens-plan-scaling-talk-slides-are-available/#comments</comments>
		<pubDate>Fri, 15 May 2009 16:13:14 +0000</pubDate>
		<dc:creator>John Bafford</dc:creator>
				<category><![CDATA[php]]></category>
		<category><![CDATA[scaling]]></category>
		<category><![CDATA[talks]]></category>
		<category><![CDATA[wordpress]]></category>

		<guid isPermaLink="false">http://bafford.com/?p=321</guid>
		<description><![CDATA[On Wednesday night, I gave a talk at the DC PHP developer&#8217;s group regarding how The Bivings Group scaled the Pickens Plan website after it stopped working following a national advertising campaign after the first 2008 presidential debate drew thousands of people to the website. The slides are now available for download. Also, I want [...]]]></description>
			<content:encoded><![CDATA[<p>On Wednesday night, I gave a talk at the DC PHP developer&#8217;s group regarding how <a href="http://www.bivings.com">The Bivings Group</a> scaled the <a href="http://www.pickensplan.com/">Pickens Plan</a> website after it stopped working following a national advertising campaign after the first 2008 presidential debate drew thousands of people to the website.</p>
<p>The <a href="http://bafford.com/talks/PickensScaling-DCPHPDG.pdf">slides</a> are now available for download.</p>
<p>Also, I want to thank everyone who showed up. It was great talking to all of you!</p>
]]></content:encoded>
			<wfw:commentRss>http://bafford.com/2009/05/15/pickens-plan-scaling-talk-slides-are-available/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>MySQL Performance Benefits of Storing Integer IP Addresses</title>
		<link>http://bafford.com/2009/03/09/mysql-performance-benefits-of-storing-integer-ip-addresses/</link>
		<comments>http://bafford.com/2009/03/09/mysql-performance-benefits-of-storing-integer-ip-addresses/#comments</comments>
		<pubDate>Mon, 09 Mar 2009 18:09:35 +0000</pubDate>
		<dc:creator>John Bafford</dc:creator>
				<category><![CDATA[mysql]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[benchmarks]]></category>

		<guid isPermaLink="false">http://bafford.com/?p=269</guid>
		<description><![CDATA[On ##php on freenode over the weekend, there was a brief discussion regarding the performance benefits of storing IP addresses in a database as an integer, rather than as a varchar. One commenter was making the argument that the numeric-to-string and string-to-numeric conversions cost involved in storing IPs as integers was more significant than the [...]]]></description>
			<content:encoded><![CDATA[<p>On ##php on freenode over the weekend, there was a brief discussion regarding the performance benefits of storing IP addresses in a database as an integer, rather than as a varchar. One commenter was making the argument that the numeric-to-string and string-to-numeric conversions cost involved in storing IPs as integers was more significant than the space savings.</p>
<p>While the space savings are easily apparent, and I&#8217;ve seen demonstrations of how much faster search operations are, I could not recall anyone ever doing an analysis of how long it takes to insert IPs as strings or integers into the database. Naturally, I had to determine an answer to this so I&#8217;d know for future reference.</p>
<p>My findings show that, beyond the disk space savings and the much faster index queries possible with integer-based IP addresses, even with the database doing the string-to-numeric conversion, it is 9-12% faster to store IP addresses into the database as integers, rather than strings.</p>
<h3>Background</h3>
<p>There are two ways you can store an IPv4 address in a database. In its native format, an IP address is a four-byte long integer, and can be represented as an <code>int unsigned</code>.</p>
<p>In its human-readable format, an IP address is a string, with a minimum length of 7 characters (0.0.0.0) and a maximum length of 15 (255.255.255.255). This gives it an average length (assuming uniform random distribution) of 13.28 characters. Accordingly, one would store it in a database field of type <code>varchar(15)</code>. In order for the database to keep track of exactly how much data is in the column, an additional byte of data must be added to store the length of the string. This brings the actual data-storage costs of an IP represented as a string to an average of 14.28 bytes (assuming the characters can be represented by one byte per character, as with latin1 or utf8).</p>
<p>This means that storing an IP address as a string requires, on average, about 10 bytes of extra data. In an application that saves access logs, that 10 bytes of data will eventually add up, and that should be reason enough to store IPs as an integer, rather than a string.</p>
<h3>Disk Space Is Cheap, So No Big Deal, Right?</h3>
<p>There are other costs associated with having larger data fields. If the column is indexed, the index will be larger as well. Larger indexes tend to perform slower than smaller indexes. Additionally, while disk space is plentiful and cheap, RAM is considerably more limited, so more memory will be used to cache the data or indexes, potentially pushing other, more valuable content out of the cache.</p>
<p>Further, while disks have been gradually getting faster, it still takes a relatively long time to read data from (and even longer to write to) a disk, and CPUs have gotten faster much more quickly than disks. The more data that has to be moved around, the more time the CPU wastes moving that data instead of performing more interesting work.</p>
<h3>So, What&#8217;s the Actual Cost?</h3>
<p>I wrote a PHP script that generates and inserts 1,000,000 random IP addresses into four mysql tables and timed the results. All tests were done with PHP 5.3alpha2 and MySQL 5.0.67 on a 2.16 GHz MacBook Pro (Intel Core Duo), with MyISAM tables.</p>
<p>The test uses four tables as described below. Each table has three columns: an <code>id int unsigned not null auto_increment primary key</code> column, and <code>ip</code> and <code>s</code> as described below:</p>
<table border="0">
<tbody>
<tr>
<th>table</th>
<th>ip</th>
<th>s</th>
<th>description</th>
</tr>
<tr>
<td>inetbench_long</td>
<td>int unsigned not null</td>
<td>char(1) not null</td>
<td>fixed row length</td>
</tr>
<tr>
<td>inetbench_long_dynamic</td>
<td>int unsigned not null</td>
<td>varchar(1) not null</td>
<td>dynamic row length</td>
</tr>
<tr>
<td>inetbench_varchar</td>
<td>varchar(15) not null</td>
<td>varchar(1) not null</td>
<td>dynamic row length; IP stored as string</td>
</tr>
<tr>
<td>inetbench_varchar_utf8</td>
<td>varchar(15) not null</td>
<td>varchar(1) not null</td>
<td>same as inetbench_varchar, but charset = utf8</td>
</tr>
</tbody>
</table>
<p>The PHP benchmark script generates 1 million random IP addresses, and then inserts that data into MySQL To make MySQL do as much work as possible, for the inetbench_long tables, the sql query string uses INET_ATON() to convert the IP to an integer, rather than attempting to do it in PHP. </p>
<p>The results:</p>
<table border="0">
<tbody>
<tr>
<th>table</th>
<th>insert time</th>
<th>avg row length</th>
<th>data length</th>
<th>index length</th>
<th>total length</th>
</tr>
<tr>
<td>inetbench_long</td>
<td>132.35 sec</td>
<td>10 bytes</td>
<td>10,000,000</td>
<td>22,300,672</td>
<td>32,300,627</td>
</tr>
<tr>
<td>inetbench_long_dynamic</td>
<td>132.71 sec</td>
<td>20 bytes</td>
<td>20,000,000</td>
<td>22,300,672</td>
<td>42,300,627</td>
</tr>
<tr>
<td>inetbench_varchar</td>
<td>144.44 sec</td>
<td>24 bytes</td>
<td>24,504,148</td>
<td>36,341,760</td>
<td>60,845,908</td>
</tr>
<tr>
<td>inetbench_varchar_utf8</td>
<td>148.86 sec</td>
<td>24 bytes</td>
<td>24,504,148</td>
<td>36,341,760</td>
<td>60,845,908</td>
</tr>
</tbody>
</table>
<p>MySQL is adding 1 byte of overhead per row to <code>inetbench_long</code>, 10 bytes/row to <code>inetbench_long_dynamic</code>, and an average of 5 bytes/row of overhead to <code>inetbench_varchar</code> and <code>inetbench_varchar_utf8</code>.</p>
<p>The <code>s</code> column was added to test the performance difference of fixed vs. dynamic row storage, but it turns out there&#8217;s not much difference.</p>
<p>You can download the code used to generate these results <a href="/downloads/mysql-inetaton-bench.php">here</a> (2 kb zip).</p>
<h3>Analysis</h3>
<p>Storing IPs as a string, besides requiring more disk space, takes 9% longer than storing them as integers, even with the overhead of converting the IP from a string to an integer. If the table uses utf8 encoding, it&#8217;s 12% slower. (This should not be surprising: UTF-8 is inherently slower to process than a strictly 8-bit encoding.) Storing data as an integer in a table with a dynamic row length is not appreciably slower. The indexes on the string tables are 63% larger.</p>
<p>For good measure, I tested a few select queries against the tables. The results were actually somewhat interesting.</p>
<p>Doing a search where you&#8217;re looking for a specific IP, or a range of IPs that can be satisfied with a like clause resulted in no significant difference between the integer and varchar storage. (This was somewhat surprising to me.)</p>
<p><code><br />
select benchmark(10000000, (select count(*) from inetbench_long where ip between inet_aton('172.0.0.0') and inet_aton('172.255.255.255')));<br />
1 row in set (0.41 sec)</p>
<p>select benchmark(10000000, (select count(*) from inetbench_varchar where ip like '172.%'));<br />
1 row in set (0.42 sec)<br />
</code></p>
<p>However, as expected, when the search range can&#8217;t be represented with a simple like query, the speed difference between numeric and string indexes really show:</p>
<p><code><br />
select benchmark(35000000, (select count(*) from inetbench_long where ip between inet_aton('172.16.0.0') and inet_aton('172.31.255.255')));<br />
1 row in set (1.43 sec)</p>
<p>select count(*) from inetbench_varchar where inet_aton(ip) between inet_aton('172.16.0.0') and inet_aton('172.31.255.255');<br />
1 row in set (1.47 sec)</p>
<p>select count(*) from inetbench_varchar_utf8 where inet_aton(ip) between inet_aton('172.16.0.0') and inet_aton('172.31.255.255');<br />
1 row in set (1.72 sec)<br />
</code></p>
<p>This results in an integer search that&#8217;s about <em>35 million</em> times faster than the string search. Also, the utf8 table is about 17% slower than the latin1 table (which isn&#8217;t too surprising, given UTF-8&#8242;s overhead).</p>
<h3>In Conclusion&#8230;</h3>
<p>With &#8220;only&#8221; a difference of 12 microseconds per insert query, it may not make sense to change an existing database if you&#8217;re not doing many queries against stored IPs. If you&#8217;re doing IP range queries, though, you probably want to convert your tables. Any new development should be storing IP addresses in the database as integers by default. The space and time savings are worth it.</p>
<p>Once IPv6 becomes more prevalent, the savings will only become larger: a 128-bit (16 byte) IPv6 address can be up to 39 characters long when represented in a &#8220;human readable&#8221; format. (Storing IPv6 addresses in the database is going to be a bit more difficult, as MySQL doesn&#8217;t have a native 16-byte-wide data type.)</p>
<p>The load on MySQL when inserting integer IPs could likely be slightly reduced by doing that conversion in your application, rather than using MySQL&#8217;s INET_ATON() function.</p>
]]></content:encoded>
			<wfw:commentRss>http://bafford.com/2009/03/09/mysql-performance-benefits-of-storing-integer-ip-addresses/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Aggregate Map Tools, Part 1: GlobalMapTiles</title>
		<link>http://bafford.com/2009/02/26/aggregate-map-tools-part-1-globalmaptiles/</link>
		<comments>http://bafford.com/2009/02/26/aggregate-map-tools-part-1-globalmaptiles/#comments</comments>
		<pubDate>Thu, 26 Feb 2009 20:53:29 +0000</pubDate>
		<dc:creator>John Bafford</dc:creator>
				<category><![CDATA[Software]]></category>
		<category><![CDATA[javascript]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[google maps]]></category>
		<category><![CDATA[open source]]></category>
		<category><![CDATA[python]]></category>

		<guid isPermaLink="false">http://bafford.com/?p=184</guid>
		<description><![CDATA[My fourth open source release this week comes from work I&#8217;ve done for my employer, The Bivings Group. Today, we are releasing a set of code that assists with aggregating markers on a Google Map. Our clients wanted to be able to display markers on a map reflecting the locations of people who provided their [...]]]></description>
			<content:encoded><![CDATA[<p>My fourth open source release this week comes from work I&#8217;ve done for my employer, <a href="http://www.bivings.com/">The Bivings Group</a>. Today, we are releasing a set of code that assists with aggregating markers on a Google Map. Our clients wanted to be able to display markers on a map reflecting the locations of people who provided their location (city, state, zip, and in some cases, street address), but with tens of thousands of expected sign-ups, it&#8217;s not feasible to display all the points on the map at once.</p>
<p><span id="more-184"></span></p>
<p>One of the challenges with displaying a map with lots of markers is that if there are too many markers in one location, the placemarks themselves quickly become an unmanageable sea that obscures the map. In some cases, this is the desired effect, but in our case, we wanted to present a cleaner map that showed a single marker that provided an indication of the number of people present in an area. (Less markers also has the advantage of loading faster.) </p>
<p>One solution to this problem is, instead of showing all of the markers all at once, you can group all of the markers that are close together into one. But, how do you determine if two locations are &#8220;close together&#8221; so that you can do that aggregation?</p>
<h3>Geohash</h3>
<p>One way is to use a <a href="http://en.wikipedia.org/wiki/Geohash">geohash</a>, which takes a latitude and longitude coordinate and, using a hashing algorithm, mixes the two numbers together into a a string that represents the position. More accurately, it represents a box which contains the coordinate in question. For example, the coordinates of the TBG office (38.9193540 °N,  77.0705180 °W) is represented by the geohash <code>dqcjqjncwv2t9</code>. This string has an interesting property: as you remove characters from the string, you get a less accurate representation of the location &#8211; the box gets larger. With a list of addresses stored in geohash format, you can do a search for all of the addresses whose geohashes all start with the same characters.</p>
<p>However, because of the way geohashes are encoded, different geohashes have different shapes, there is some overlap between different geohashes, and in some cases, removing a character from a geohash can result in a larger shape on the map that doesn&#8217;t cover the entirety of the more specific geohash. As a result, we determined that they weren&#8217;t suitable for our needs: because of the overlap, it was possible for one location to appear to be in the space covered by multiple geohashes, thus potentially leading to unreliable counting.</p>
<h3>Quadtree</h3>
<p>Some further searching led me to <a href="http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/">maptiler.org</a>, which provided an alternate answer. Since the Earth is a sphere, it can be broken up into quadrants (corresponding to the four hemispheres: north west, north east, south west, and south east), which (with some stretching) are square. Each of those quadrants can be subdivided into subquadrants, and those into more subquadrants, each of which contains one quarter of the total area covered by the quadrant that contains it.</p>
<p>In fact, this is how Google Maps map tiles work. When zoomed all the way out, the entire earth is represented as a 256 x 256 pixel map tile image. Each larger zoom level contains four times the number of map tiles as the previous zoom level, with no overlap. At the maximum zoom level (20), the entire Earth is represented by 4<sup>19</sup> = 274,877,906,944 256 x 256 pixel images.</p>
<p>How does that help us? Well, if we number each of the quadrants (0 = NW, 1 = NE, 2 = SW, 3 = SE), and add a digit for each sub-quadrant we &#8220;zoom in&#8221; to, we get a string that represents a square on the map at a zoom level equivalent to the length of the string. For example, the coordinates of the TBG office can be represented as <code>0320100322312003121</code>. If we remove a character from the hash, it&#8217;s guaranteed that the larger space contains all the possible locations in the smaller space. There&#8217;s no overlap between hashes of the same length. This means we&#8217;re guaranteed the accurate counting we wanted. And we can very conveniently scale our searches of the database of addresses with the zoom level of the map window we&#8217;re viewing the results in.</p>
<h3>Pulling it all Together: GlobalMapTiles</h3>
<p>On the client&#8217;s map page, whenever the map position or zoom level changes, we send a few ajax queries to the web server with quadtree codes covering the map window. The server obligingly searches the database for the number of people in the area represented by the code, and returns a result list containing the number of people in the quadtree a level or two deeper. We then take the results, convert the quadtree code back into a coordinate on the map, and add a marker at its center.</p>
<p>As luck would have it, there&#8217;s a Python script, <code>globalmaptiles.py</code> on the maptiler.org page that handles the conversion from latitude and longitude to these quadtree coordinates. Although Python is a fine language (and some of the most fun code I&#8217;ve written has been in Python), we needed this code in PHP and JavaScript. But that&#8217;s ok. The code is almost entirely simple mathematical operations, and that&#8217;s very easy to port from Python to JavaScript and PHP. This port, plus some additions of my own to facilitate turning a quadtree string back into its bounding coordinates on the map is what we&#8217;re releasing today.</p>
<p>This is, of course, only half of the solution. We now have a system by which we can identify locations on the map which are nearby and aggregate them together, but we still need a way to pull that information out of the database and send it on-demand to the web browser to be displayed. That&#8217;s a more interesting problem (and solution). I&#8217;m still refining that code and adding additional functionality, and we&#8217;ll release that soon, when we&#8217;re satisfied with it.</p>
<p>As the original implementation was licensed with the X11 license, GlobalMapTiles is also licensed under the X11 license. For more details on how to use it, and to download, see the <a href="/software/aggregate-map-tools/">Aggregate Map Tools</a> page.</p>
]]></content:encoded>
			<wfw:commentRss>http://bafford.com/2009/02/26/aggregate-map-tools-part-1-globalmaptiles/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
