Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 269 lines (241 sloc) 13.155 kb
c04dd9f initial commit
David Flanagan authored
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <title>Zipcode Database</title>
5 <script>
6 // IndexedDB implementations still use API prefixes
7 var indexedDB = window.indexedDB || // Use the standard DB API
8 window.mozIndexedDB || // Or Firefox's early version of it
9 window.webkitIndexedDB; // Or Chrome's early version
10 // Firefox does not prefix these two:
11 var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
12 var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
13
14 // We'll use this function to log any database errors that occur
15 function logerr(e) {
16 console.log("IndexedDB error" + e.code + ": " + e.message);
17 }
18
19 // This function asynchronously obtains the database object (creating and
20 // initializing the database if necessary) and passes it to the function f().
21 function withDB(f) {
22 var request = indexedDB.open("zipcodes"); // Request the zipcode database
23 request.onerror = logerr; // Log any errors
24 request.onsuccess = function() { // Or call this when done
25 var db = request.result; // The result of the request is the database
26
27 // You can always open a database, even if it doesn't exist.
28 // We check the version to find out whether the DB has been created and
29 // initialized yet. If not, we have to go do that. But if the db is
30 // already set up, we just pass it to the callback function f().
31 if (db.version === "1") f(db); // If db is inited, pass it to f()
32 else initdb(db,f); // Otherwise initialize it first
33 }
34 }
35
36 // Given a zip code, find out what city it belongs to and asynchronously
37 // pass the name of that city to the specified callback function.
38 function lookupCity(zip, callback) {
39 withDB(function(db) {
40 // Create a transaction object for this query
41 var transaction = db.transaction(["zipcodes"], // Object stores we need
42 IDBTransaction.READ_ONLY, // No updates
43 0); // No timeout
44
45 // Get the object store from the transaction
46 var objects = transaction.objectStore("zipcodes");
47
48 // Now request the object that matches the specified zipcode key.
49 // The lines above were synchronous, but this one is async
50 var request = objects.get(zip);
51
52 request.onerror = logerr; // Log any errors that occur
53 request.onsuccess = function() { // Pass the result to this function
54 // The result object is now in the request.result
55 var object = request.result;
56 if (object) // If we found a match, pass city and state to callback
57 callback(object.city + ", " + object.state);
58 else // Otherwise, tell the callback that we failed
59 callback("Unknown zip code");
60 }
61 });
62 }
63
64 // Given the name of a city find all zipcodes for all cities (in any state)
65 // with that name (case-sensitive). Asynchronously pass the results, one at
66 // a time, to the specified callback function
67 function lookupZipcodes(city, callback) {
68 withDB(function(db) {
69 // As above, we create a transaction and get the object store
70 var transaction = db.transaction(["zipcodes"],
71 IDBTransaction.READ_ONLY, 0);
72 var store = transaction.objectStore("zipcodes");
73 // This time we get the city index of the object store
74 var index = store.index("cities");
75
76 // This query is likely to have many results, so we have to use a
77 // cursor object to retrieve them all. To create a cursor, we need
78 // a range object that represents the range of keys
79 var range = new IDBKeyRange.only(city); // A range with only() one key
80
81 // Everything above has been synchronous.
82 // Now we request a cursor, which will be returned asynchronously.
83 var request = index.openCursor(range); // Request the cursor
84 request.onerror = logerr; // Log errors
85 request.onsuccess = function() { // Pass cursor to this function
86 // This event handler will be invoked multiple times, once
87 // for each record that matches the query, and then once more
88 // with a null cursor to indicate that we're done.
89 var cursor = request.result // The cursor is in request.result
90 if (!cursor) return; // No cursor means no more results
91 var object = cursor.value // Get the matching record
92 callback(object); // Pass it to the callback
93 cursor.continue(); // Ask for the next matching record
94 };
95 });
96 }
97
98 // This function is used by an onchange callback in the document below
99 // It makes a DB request and displays the result
100 function displayCity(zip) {
101 lookupCity(zip, function(s) { document.getElementById('city').value = s; });
102 }
103
104 // This is another onchange callback used in the document below.
105 // It makes a DB request and displays the results
106 function displayZipcodes(city) {
107 var output = document.getElementById("zipcodes");
108 output.innerHTML = "Matching zipcodes:";
109 lookupZipcodes(city, function(o) {
110 var div = document.createElement("div");
111 var text = o.zipcode + ": " + o.city + ", " + o.state;
112 div.appendChild(document.createTextNode(text));
113 output.appendChild(div);
114 });
115 }
116
117 // Set up the structure of the database and populate it with data, then pass
118 // the database to the function f(). withDB() calls this function if the
119 // database has not been initialized yet. This is the trickiest part of the
120 // program, so we've saved it for last.
121 function initdb(db, f) {
122 // Downloading zipcode data and storing it in the database can take
123 // a while the first time a user runs this application. So we have to
124 // provide notification while that is going on.
125 var statusline = document.createElement("div");
126 statusline.style.cssText =
127 "position:fixed; left:0px; top:0px; width:100%;" +
128 "color:white; background-color: black; font: bold 18pt sans-serif;" +
129 "padding: 10px; ";
130 document.body.appendChild(statusline);
131 function status(msg) { statusline.innerHTML = msg.toString(); };
132
133 status("Initializing zipcode database");
134
135 // The only time you can define or alter the structure of an IndexedDB
136 // database is in the onsucess handler of a setVersion request.
137 var request = db.setVersion("1"); // Try to update the DB version
138 request.onerror = status; // Display status on fail
139 request.onsuccess = function() { // Otherwise call this function
140 // Our zipcode database includes only one object store.
141 // It will hold objects that look like this: {
142 // zipcode: "02134", // send it to Zoom! :-)
143 // city: "Allston",
144 // state: "MA",
145 // latitude: "42.355147",
146 // longitude: "-71.13164"
147 // }
148 //
149 // We'll use the "zipcode" property as the database key
150 // And we'll also create an index using the city name
151
152 // Create the object store, specifying a name for the store and
153 // an options object that includes the "key path" specifying the
154 // property name of the key field for this store. (If we omit the
155 // key path, IndexedDB will define its own unique integer key.)
156 var store = db.createObjectStore("zipcodes", // store name
157 { keyPath: "zipcode" });
158
159 // Now index the object store by city name as well as by zipcode.
160 // With this method the key path string is passed directly as a
161 // required argument rather than as part of an options object.
162 store.createIndex("cities", "city");
163
164 // Now we need to download our zipcode data, parse it into objects
165 // and store those objects in object store we created above.
166 //
167 // Our file of raw data contains lines formatted like this:
168 //
169 // 02130,Jamaica Plain,MA,42.309998,-71.11171
170 // 02131,Roslindale,MA,42.284678,-71.13052
171 // 02132,West Roxbury,MA,42.279432,-71.1598
172 // 02133,Boston,MA,42.338947,-70.919635
173 // 02134,Allston,MA,42.355147,-71.13164
174 //
175 // Surprisingly, the US Postal Service does not make this data freely
176 // available, so we use out-of-date census-based zipcode data from:
177 // http://mappinghacks.com/2008/04/28/civicspace-zip-code-database/
178
179 // We use XMLHttpRequest to download the data. But use the new XHR2
180 // onload and onprogress events to process it as it arrives
181 var xhr = new XMLHttpRequest(); // An XHR to download the data
182 xhr.open("GET", "zipcodes.csv"); // HTTP GET for this URL
183 xhr.send(); // Start right away
184 xhr.onerror = status; // Display any error codes
185 var lastChar = 0, numlines = 0; // How much have we already processed?
186
187 // Handle the database file in chunks as it arrives
188 xhr.onprogress = xhr.onload = function(e) { // Two handlers in one!
189 // We'll process the chunk between lastChar and the last newline
190 // that we've received. (We need to look for newlines so we don't
191 // process partial records)
192 var lastNewline = xhr.responseText.lastIndexOf("\n");
193 if (lastNewline > lastChar) {
194 var chunk = xhr.responseText.substring(lastChar, lastNewline)
195 lastChar = lastNewline + 1; // Where to start next time
196
197 // Now break the new chunk of data into individual lines
198 var lines = chunk.split("\n");
199 numlines += lines.length;
200
201 // In order to insert zipcode data into the database we need a
202 // transaction object. All the database insertions we make
203 // using this object will be commited to the database when this
204 // function returns and the browser goes back to the event
205 // loop. To create our transaction object, we need to specify
206 // which object stores we'll be using (we only have one) and we
207 // need to tell it that we'll be doing writes to the database,
208 // not just reads:
209 var transaction = db.transaction(["zipcodes"], // object stores
210 IDBTransaction.READ_WRITE);
211
212 // Get our object store from the transaction
213 var store = transaction.objectStore("zipcodes");
214
215 // Now loop through the lines of the zipcode file, create
216 // objects for them, and add them to the object store.
217 for(var i = 0; i < lines.length; i++) {
218 var fields = lines[i].split(","); // Comma-separated values
219 var record = { // This is the object we'll store
220 zipcode: fields[0], // All properties are string
221 city: fields[1],
222 state: fields[2],
223 latitude: fields[3],
224 longitude: fields[4]
225 };
226
227 // The best part about the IndexedDB API is that object
228 // stores are *really* simple. Here's how we add a record:
229 store.put(record); // Or use add() to avoid overwriting
230 }
231
232 status("Initializing zipcode database: loaded "
233 + numlines + " records.");
234 }
235
236 if (e.type == "load") {
237 // If this was the final load event, then we've sent all our
238 // zipcode data to the database. But since we've just blasted
239 // it with some 40,000 records, it may still be processing.
240 // So we'll make a simple query. When it succeeds, we know
241 // that the database is ready to go, and we can then remove
242 // the status line and finally call the function f() that was
243 // passed to withDB() so long ago
244 lookupCity("02134", function(s) { // Allston, MA
245 document.body.removeChild(statusline);
246 withDB(f);
247 });
248 }
249 }
250 }
251 }
252 </script>
253 </head>
254 <body>
255 <p>Enter a zip code to find its city:</p>
256 Zipcode: <input onchange="displayCity(this.value)"></input>
257 City: <output id="city"></output>
258 </div>
259 <div>
260 <p>Enter a city name (case sensitive, without state) to find cities and their zipcodes:</p>
261 City: <input onchange="displayZipcodes(this.value)"></input>
262 <div id="zipcodes"></div>
263 </div>
264 <p><i>This example is only known to work in Firefox 4 and Chrome 11.</i></p>
265 <p><i>Your first query may take a very long time to complete.</i></p>
266 <p><i>You may need to start Chrome with --unlimited-quota-for-indexeddb</i></p>
267 </body>
268 </html>
Something went wrong with that request. Please try again.