add support for saving prefs and bookmarks in an indexedDB (retrieving is not done...
[mandelbrot-web.git] / js / mandelbrot.js
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 // Get the best-available indexedDB object.
6 var iDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;
7 var mainDB;
8
9 var gMainCanvas, gMainContext;
10 var gColorPalette = [];
11 var gStartTime = 0;
12 var gCurrentImageData;
13 var gLastImageData;
14
15 function Startup() {
16   initDB();
17
18   gMainCanvas = document.getElementById("mbrotImage");
19   gMainContext = gMainCanvas.getContext("2d");
20
21   gMainCanvas.addEventListener("mouseup", imgEvHandler, false);
22   gMainCanvas.addEventListener("mousedown", imgEvHandler, false);
23   gMainCanvas.addEventListener("mousemove", imgEvHandler, false);
24   gMainCanvas.addEventListener("touchstart", imgEvHandler, false);
25   gMainCanvas.addEventListener("touchend", imgEvHandler, false);
26   gMainCanvas.addEventListener("touchcancel", imgEvHandler, false);
27   gMainCanvas.addEventListener("touchleave", imgEvHandler, false);
28   gMainCanvas.addEventListener("touchmove", imgEvHandler, false);
29
30   var initTile = new Image();
31   initTile.src = "style/initial-overview.png";
32   gMainContext.drawImage(initTile, 0, 0);
33 }
34
35 function initDB() {
36   // Open DB.
37   var request = iDB.open("MainDB", 1);
38   request.onerror = function(event) {
39     // Errors can be handled here. Error codes explain in:
40     // https://developer.mozilla.org/en/IndexedDB/IDBDatabaseException#Constants
41     //document.getElementById("debug").textContent =
42     //  "error opening mainDB: " + event.target.errorCode;
43   };
44   request.onsuccess = function(event) {
45     //document.getElementById("debug").textContent = "mainDB opened.";
46     mainDB = request.result;
47   };
48   request.onupgradeneeded = function(event) {
49     mainDB = request.result;
50     //document.getElementById("debug").textContent = "mainDB upgraded.";
51     // Create a "prefs" objectStore.
52     var prefsStore = mainDB.createObjectStore("prefs");
53     // Create a "bookmarks" objectStore.
54     var bmStore = mainDB.createObjectStore("bookmarks");
55     mainDB.onversionchange = function(event) {
56       mainDB.close();
57       mainDB = undefined;
58       initDB();
59     };
60   };
61 }
62
63 function getAdjustVal(aName) {
64   var value;
65   switch (aName) {
66     case "image.width":
67     case "image.height":
68       value = 0;
69       try {
70         value = document.getElementById(aName.replace(".", "_")).value;
71       }
72       catch (e) { }
73       if ((value < 10) || (value > 5000)) {
74         value = 300;
75         gPrefs.set(prefname, value);
76         //document.getElementById(aName.replace(".", "_")).value = value;
77       }
78       return value;
79     case "last_image.Cr_*":
80       var Cr_min = -2.0;
81       var Cr_max = 1.0;
82       try {
83         Cr_min = parseFloat(document.getElementById("Cr_min").value);
84         Cr_max = parseFloat(document.getElementById("Cr_max").value);
85       }
86       catch (e) { }
87       if ((Cr_min < -3) || (Cr_min > 2) ||
88           (Cr_max < -3) || (Cr_max > 2) || (Cr_min >= Cr_max)) {
89         Cr_min = -2.0; Cr_max = 1.0;
90       }
91       gPrefs.set("Cr_min", Cr_min);
92       gPrefs.set("Cr_max", Cr_max);
93       document.getElementById("Cr_min").value = Cr_min;
94       document.getElementById("Cr_max").value = Cr_max;
95       return {Cr_min: Cr_min, Cr_max: Cr_max};
96     case "last_image.Ci_*":
97       var Ci_min = -1.5;
98       var Ci_max = 1.5;
99       try {
100         Ci_min = parseFloat(document.getElementById("Ci_min").value);
101         Ci_max = parseFloat(document.getElementById("Ci_max").value);
102       }
103       catch (e) { }
104       if ((Ci_min < -2.5) || (Ci_min > 2.5) ||
105           (Ci_max < -2.5) || (Ci_max > 2.5) || (Ci_min >= Ci_max)) {
106         Ci_min = -1.5; Ci_max = 1.5;
107       }
108       gPrefs.set("Ci_min", Ci_min);
109       gPrefs.set("Ci_max", Ci_max);
110       document.getElementById("Ci_min").value = Ci_min;
111       document.getElementById("Ci_max").value = Ci_max;
112       return {Ci_min: Ci_min, Ci_max: Ci_max};
113     case "iteration_max":
114       value = 500;
115       try {
116         value = document.getElementById("iterMax").value;
117       }
118       catch (e) {
119         setIter(value);
120       }
121       if (value < 10 || value > 10000) {
122         value = 500;
123         setIter(value);
124       }
125       return value;
126     case "use_algorithm":
127       value = "numeric";
128       try {
129         value = document.getElementById("algorithm").value;
130       }
131       catch (e) {
132         setAlgorithm(value);
133       }
134       return value;
135    case "color_palette":
136       value = "kairo";
137       try {
138         value = document.getElementById("palette").value;
139       }
140       catch(e) {
141         setPalette(value);
142       }
143       return value;
144    case "syncProportions":
145       value = true;
146       try {
147         value = document.getElementById("proportional").value;
148       }
149       catch(e) {
150         gPrefs.set(prefname, value);
151         document.getElementById("proportional").value = value;
152       }
153       return value;
154     default:
155       return false;
156   }
157 }
158
159 function setVal(aName, aValue) {
160   switch (aName) {
161     case "image.width":
162     case "image.height":
163       gPrefs.set(aName, value);
164       document.getElementById(aName.replace(".", "_")).value = value;
165       break;
166     case "last_image.Cr_*":
167       gPrefs.set("Cr_min", Cr_min);
168       gPrefs.set("Cr_max", Cr_max);
169       document.getElementById("Cr_min").value = aValue.Cr_min;
170       document.getElementById("Cr_max").value = aValue.Cr_max;
171       break;
172     case "last_image.Ci_*":
173       gPrefs.set("Ci_min", Ci_min);
174       gPrefs.set("Ci_max", Ci_max);
175       document.getElementById("Ci_min").value = aValue.Ci_min;
176       document.getElementById("Ci_max").value = aValue.Ci_max;
177       break;
178     case "iteration_max":
179       setIter(aValue);
180       break;
181     case "use_algorithm":
182       setAlgorithm(aValue);
183       break;
184    case "color_palette":
185       setPalette(valueaValue);
186       break;
187    case "syncProportions":
188       gPrefs.set(aName, value);
189       document.getElementById("proportional").value = aValue;
190       break;
191   }
192 }
193
194 function adjustCoordsAndDraw(aC_min, aC_max) {
195   var iWidth = getAdjustVal("image.width");
196   var iHeight = getAdjustVal("image.height");
197
198   // correct coordinates
199   if (aC_min.r < -2)
200     aC_min.r = -2;
201   if (aC_max.r > 2)
202     aC_max.r = 2;
203   if ((aC_min.r > 2) || (aC_max.r < -2) || (aC_min.r >= aC_max.r)) {
204     aC_min.r = -2.0; aC_max.r = 1.0;
205   }
206   if (aC_min.i < -2)
207     aC_min.i = -2;
208   if (aC_max.i > 2)
209     aC_max.i = 2;
210   if ((aC_min.i > 2) || (aC_max.i < -2) || (aC_min.i >= aC_max.i)) {
211     aC_min.i = -1.3; aC_max.i = 1.3;
212   }
213
214   var CWidth = aC_max.r - aC_min.r;
215   var CHeight = aC_max.i - aC_min.i;
216   var C_mid = new complex(aC_min.r + CWidth / 2, aC_min.i + CHeight / 2);
217
218   var CRatio = Math.max(CWidth / iWidth, CHeight / iHeight);
219
220   setVal("last_image.Cr_*", {Cr_min: C_mid.r - iWidth * CRatio / 2,
221                              Cr_max: C_mid.r + iWidth * CRatio / 2});
222   setVal("last_image.Ci_*", {Ci_min: C_mid.i - iHeight * CRatio / 2,
223                              Ci_max: C_mid.i + iHeight * CRatio / 2});
224
225   drawImage();
226 }
227
228 function drawImage() {
229   var canvas = gMainCanvas;
230   var context = gMainContext;
231
232   document.getElementById("calcTime").textContent = "--";
233
234   if (gCurrentImageData) {
235     gLastImageData = gCurrentImageData;
236     document.getElementById("backButton").disabled = false;
237   }
238
239   gColorPalette = getColorPalette(document.getElementById("palette").value);
240
241   var Cr_vals = getAdjustVal("last_image.Cr_*");
242   var Cr_min = Cr_vals.Cr_min;
243   var Cr_max = Cr_vals.Cr_max;
244
245   var Ci_vals = getAdjustVal("last_image.Ci_*");
246   var Ci_min = Ci_vals.Ci_min;
247   var Ci_max = Ci_vals.Ci_max;
248
249   var iterMax = getAdjustVal("iteration_max");
250   var algorithm = getAdjustVal("use_algorithm");
251
252   var iWidth = canvas.width;
253   if ((iWidth < 10) || (iWidth > 5000)) {
254     iWidth = 300;
255     canvas.width = iWidth;
256   }
257   var iHeight = canvas.height;
258   if ((iHeight < 10) || (iHeight > 5000)) {
259     iHeight = 300;
260     canvas.height = iHeight;
261   }
262
263   gCurrentImageData = {
264     C_min: new complex(Cr_min, Ci_min),
265     C_max: new complex(Cr_max, Ci_max),
266     iWidth: iWidth,
267     iHeight: iHeight,
268     iterMax: iterMax
269   };
270
271   context.fillStyle = "rgba(255, 255, 255, 127)";
272   context.fillRect(0, 0, canvas.width, canvas.height);
273
274   gStartTime = new Date();
275
276   drawLine(0, [Cr_min, Cr_max, Ci_min, Ci_max],
277               canvas, context, iterMax, algorithm);
278 }
279
280 function drawLine(line, dimensions, canvas, context, iterMax, algorithm) {
281     var Cr_min = dimensions[0];
282     var Cr_max = dimensions[1];
283     var Cr_scale = Cr_max - Cr_min;
284
285     var Ci_min = dimensions[2];
286     var Ci_max = dimensions[3];
287     var Ci_scale = Ci_max - Ci_min;
288
289     var lines = Math.min(canvas.height - line, 8);
290     var imageData = context.createImageData(canvas.width, lines);
291     var pixels = imageData.data;
292     var idx = 0;
293     for (var img_y = line; img_y < canvas.height && img_y < line+8; img_y++)
294       for (var img_x = 0; img_x < canvas.width; img_x++) {
295         var C = new complex(Cr_min + (img_x / canvas.width) * Cr_scale,
296                             Ci_min + (img_y / canvas.height) * Ci_scale);
297         var colors = drawPoint(context, img_x, img_y, C, iterMax, algorithm);
298         pixels[idx++] = colors[0];
299         pixels[idx++] = colors[1];
300         pixels[idx++] = colors[2];
301         pixels[idx++] = colors[3];
302       }
303     context.putImageData(imageData, 0, line);
304
305     if (img_y < canvas.height)
306       setTimeout(drawLine, 0, img_y, dimensions, canvas, context, iterMax, algorithm);
307     else if (gStartTime)
308       EndCalc();
309 }
310
311 function EndCalc() {
312   var endTime = new Date();
313   var timeUsed = (endTime.getTime() - gStartTime.getTime()) / 1000;
314   document.getElementById("calcTime").textContent = timeUsed.toFixed(3) + " seconds";
315 }
316
317 function complex(aReal, aImag) {
318   this.r = aReal;
319   this.i = aImag;
320 }
321 complex.prototype = {
322   square: function() {
323     return new complex(this.r * this.r - this.i * this.i,
324                        2 * this.r * this.i);
325   },
326   dist: function() {
327     return Math.sqrt(this.r * this.r + this.i * this.i);
328   },
329   add: function(aComplex) {
330     return new complex(this.r + aComplex.r, this.i + aComplex.i);
331   }
332 }
333
334 function mandelbrotValueOO (aC, aIterMax) {
335   // this would be nice code in general but it looks like JS objects are too heavy for normal use.
336   var Z = new complex(0.0, 0.0);
337   for (var iter = 0; iter < aIterMax; iter++) {
338     Z = Z.square().add(aC);
339     if (Z.r * Z.r + Z.i * Z.i > 256) { break; }
340   }
341   return iter;
342 }
343
344 function mandelbrotValueNumeric (aC, aIterMax) {
345   // optimized numeric code for fast calculation
346   var Cr = aC.r, Ci = aC.i;
347   var Zr = 0.0, Zi = 0.0;
348   var Zr2 = Zr * Zr, Zi2 = Zi * Zi;
349   for (var iter = 0; iter < aIterMax; iter++) {
350     Zi = 2 * Zr * Zi + Ci;
351     Zr = Zr2 - Zi2 + Cr;
352
353     Zr2 = Zr * Zr; Zi2 = Zi * Zi;
354     if (Zr2 + Zi2 > 256) { break; }
355   }
356   return iter;
357 }
358
359 function getColor(aIterValue, aIterMax) {
360   var standardizedValue = Math.round(aIterValue * 1024 / aIterMax);
361   if (gColorPalette && gColorPalette.length)
362     return gColorPalette[standardizedValue];
363
364   // fallback to simple b/w if for some reason we don't have a palette
365   if (aIterValue == aIterMax)
366     return [0, 0, 0, 255];
367   else
368     return [255, 255, 255, 255];
369 }
370
371 function getColorPalette(palName) {
372   var palette = [];
373   switch (palName) {
374     case 'bw':
375       for (var i = 0; i < 1024; i++) {
376         palette[i] = [255, 255, 255, 255];
377       }
378       palette[1024] = [0, 0, 0, 255];
379       break;
380     case 'kairo':
381       // outer areas
382       for (var i = 0; i < 32; i++) {
383         var cc1 = Math.floor(i * 127 / 31);
384         var cc2 = 170 - Math.floor(i * 43 / 31);
385         palette[i] = [cc1, cc2, cc1, 255];
386       }
387       // inner areas
388       for (var i = 0; i < 51; i++) {
389         var cc = Math.floor(i * 170 / 50);
390         palette[32 + i] = [cc, 0, (170-cc), 255];
391       }
392       // corona
393       for (var i = 0; i < 101; i++) {
394         var cc = Math.floor(i * 200 / 100);
395         palette[83 + i] = [255, cc, 0, 255];
396       }
397       // inner corona
398       for (var i = 0; i < 201; i++) {
399         var cc1 = 255 - Math.floor(i * 85 / 200);
400         var cc2 = 200 - Math.floor(i * 30 / 200);
401         var cc3 = Math.floor(i * 170 / 200);
402         palette[184 + i] = [cc1, cc2, cc3, 255];
403       }
404       for (var i = 0; i < 301; i++) {
405         var cc1 = 170 - Math.floor(i * 43 / 300);
406         var cc2 = 170 + Math.floor(i * 85 / 300);
407         palette[385 + i] = [cc1, cc1, cc2, 255];
408       }
409       for (var i = 0; i < 338; i++) {
410         var cc = 127 + Math.floor(i * 128 / 337);
411         palette[686 + i] = [cc, cc, 255, 255];
412       }
413       palette[1024] = [0, 0, 0, 255];
414       break;
415     case 'rainbow-linear1':
416       for (var i = 0; i < 256; i++) {
417         palette[i] = [i, 0, 0, 255];
418         palette[256 + i] = [255, i, 0, 255];
419         palette[512 + i] = [255 - i, 255, i, 255];
420         palette[768 + i] = [i, 255-i, 255, 255];
421       }
422       palette[1024] = [0, 0, 0, 255];
423       break;
424     case 'rainbow-squared1':
425       for (var i = 0; i < 34; i++) {
426         var cc = Math.floor(i * 255 / 33);
427         palette[i] = [cc, 0, 0, 255];
428       }
429       for (var i = 0; i < 137; i++) {
430         var cc = Math.floor(i * 255 / 136);
431         palette[34 + i] = [255, cc, 0, 255];
432       }
433       for (var i = 0; i < 307; i++) {
434         var cc = Math.floor(i * 255 / 306);
435         palette[171 + i] = [255 - cc, 255, cc, 255];
436       }
437       for (var i = 0; i < 546; i++) {
438         var cc = Math.floor(i * 255 / 545);
439         palette[478 + i] = [cc, 255 - cc, 255, 255];
440       }
441       palette[1024] = [0, 0, 0, 255];
442       break;
443     case 'rainbow-linear2':
444       for (var i = 0; i < 205; i++) {
445         var cc = Math.floor(i * 255 / 204);
446         palette[i] = [255, cc, 0, 255];
447         palette[204 + i] = [255 - cc, 255, 0, 255];
448         palette[409 + i] = [0, 255, cc, 255];
449         palette[614 + i] = [0, 255 - cc, 255, 255];
450         palette[819 + i] = [cc, 0, 255, 255];
451       }
452       palette[1024] = [0, 0, 0, 255];
453       break;
454     case 'rainbow-squared2':
455       for (var i = 0; i < 19; i++) {
456         var cc = Math.floor(i * 255 / 18);
457         palette[i] = [255, cc, 0, 255];
458       }
459       for (var i = 0; i < 74; i++) {
460         var cc = Math.floor(i * 255 / 73);
461         palette[19 + i] = [255 - cc, 255, 0, 255];
462       }
463       for (var i = 0; i < 168; i++) {
464         var cc = Math.floor(i * 255 / 167);
465         palette[93 + i] = [0, 255, cc, 255];
466       }
467       for (var i = 0; i < 298; i++) {
468         var cc = Math.floor(i * 255 / 297);
469         palette[261 + i] = [0, 255 - cc, 255, 255];
470       }
471       for (var i = 0; i < 465; i++) {
472         var cc = Math.floor(i * 255 / 464);
473         palette[559 + i] = [cc, 0, 255, 255];
474       }
475       palette[1024] = [0, 0, 0, 255];
476       break;
477   }
478   return palette;
479 }
480
481 function drawPoint(context, img_x, img_y, C, iterMax, algorithm) {
482   var itVal;
483   switch (algorithm) {
484     case 'oo':
485       itVal = mandelbrotValueOO(C, iterMax);
486       break;
487     case 'numeric':
488     default:
489       itVal = mandelbrotValueNumeric(C, iterMax);
490       break;
491   }
492   return getColor(itVal, iterMax);
493 }
494
495 // ########## UI functions ##########
496
497 var zoomstart;
498 var imgBackup;
499 var zoomTouchID;
500
501 var imgEvHandler = {
502   handleEvent: function(aEvent) {
503     var canvas = document.getElementById("mbrotImage");
504     var context = canvas.getContext("2d");
505     var touchEvent = aEvent.type.indexOf('touch') != -1;
506
507     // Bail out if this is neither a touch nor left-click.
508     if (!touchEvent && aEvent.button != 0)
509       return;
510
511     // Bail out if the started touch can't be found.
512     if (touchEvent && zoomstart &&
513         !aEvent.changedTouches.identifiedTouch(zoomTouchID))
514       return;
515
516     var coordObj = touchEvent ?
517                    aEvent.changedTouches.identifiedTouch(zoomTouchID) :
518                    aEvent;
519
520     switch (aEvent.type) {
521       case 'mousedown':
522       case 'touchstart':
523         if (touchEvent) {
524           zoomTouchID = aEvent.changedTouches.item(0).identifier;
525           coordObj = aEvent.changedTouches.identifiedTouch(zoomTouchID);
526         }
527         // left button - start dragzoom
528         zoomstart = {x: coordObj.clientX - canvas.offsetLeft,
529                      y: coordObj.clientY - canvas.offsetTop};
530         imgBackup = context.getImageData(0, 0, canvas.width, canvas.height);
531         break;
532       case 'mouseup':
533       case 'touchend':
534         if (zoomstart) {
535           context.putImageData(imgBackup, 0, 0);
536           var zoomend = {x: coordObj.clientX - canvas.offsetLeft,
537                          y: coordObj.clientY - canvas.offsetTop};
538
539           // make sure zoomend is bigger than zoomstart
540           if ((zoomend.x == zoomstart.x) || (zoomend.y == zoomstart.y)) {
541             // cannot zoom what has no area, discard it
542             zoomstart = undefined;
543             return;
544           }
545           if (zoomend.x < zoomstart.x)
546             [zoomend.x, zoomstart.x] = [zoomstart.x, zoomend.x];
547           if (zoomend.y < zoomstart.y)
548             [zoomend.y, zoomstart.y] = [zoomstart.y, zoomend.y];
549
550           if (gCurrentImageData) {
551             // determine new "coordinates"
552             var CWidth = gCurrentImageData.C_max.r - gCurrentImageData.C_min.r;
553             var CHeight = gCurrentImageData.C_max.i - gCurrentImageData.C_min.i;
554             var newC_min = new complex(
555                 gCurrentImageData.C_min.r + zoomstart.x / gCurrentImageData.iWidth * CWidth,
556                 gCurrentImageData.C_min.i + zoomstart.y / gCurrentImageData.iHeight * CHeight);
557             var newC_max = new complex(
558                 gCurrentImageData.C_min.r + zoomend.x / gCurrentImageData.iWidth * CWidth,
559                 gCurrentImageData.C_min.i + zoomend.y / gCurrentImageData.iHeight * CHeight);
560           }
561           else {
562             var newC_min = new complex(-2, -1.5);
563             var newC_max = new complex(1, 1.5);
564           }
565
566           adjustCoordsAndDraw(newC_min, newC_max);
567         }
568         zoomstart = undefined;
569         break;
570       case 'mousemove':
571       case 'touchmove':
572         if (zoomstart) {
573           context.putImageData(imgBackup, 0, 0);
574           context.strokeStyle = "rgb(255,255,31)";
575           context.strokeRect(zoomstart.x, zoomstart.y,
576                              coordObj.clientX - canvas.offsetLeft - zoomstart.x,
577                              coordObj.clientY - canvas.offsetTop - zoomstart.y);
578         }
579         break;
580     }
581   }
582 };
583
584 function drawIfEmpty() {
585   if (!gCurrentImageData) {
586     drawImage();
587   }
588 }
589
590 function toggleSettings() {
591   var fs = document.getElementById("settings");
592   if (fs.style.display != "block") {
593     fs.style.display = "block";
594   }
595   else {
596     fs.style.display = "none";
597   }
598 }
599
600 function goBack() {
601   if (gLastImageData) {
602     document.getElementById("iterMax").value = gLastImageData.iterMax;
603     // use gLastImageData.iWidth, gLastImageData.iHeight ???
604     adjustCoordsAndDraw(gLastImageData.C_min, gLastImageData.C_max);
605     gLastImageData = undefined;
606     document.getElementById("backButton").disabled = true;
607   }
608 }
609
610 function setIter(aIter) {
611   gPrefs.set("iteration_max", aIter);
612   document.getElementById("iterMax").value = aIter;
613 }
614
615 function setPalette(aPaletteID) {
616   gPrefs.set("color_palette", aPaletteID);
617   document.getElementById("palette").value = aPaletteID;
618   gColorPalette = getColorPalette(aPaletteID);
619 }
620
621 function setAlgorithm(algoID) {
622   gPrefs.set("use_algorithm", algoID);
623   //document.getElementById("algorithm").value = algoID;
624 }
625
626 var gPrefs = {
627   objStore: "prefs",
628
629   get: function(aKey, aCallback) {
630     if (!mainDB)
631       return;
632     var transaction = mainDB.transaction([this.objStore]);
633     var request = transaction.objectStore(this.objStore).get(aKey);
634     request.onsuccess = function(event) {
635       aCallback(request.result, event);
636     };
637     request.onerror = function(event) {
638       // Errors can be handled here.
639       aCallback(undefined, event);
640     };
641   },
642
643   set: function(aKey, aValue, aCallback) {
644     if (!mainDB)
645       return;
646     var success = false;
647     var transaction = mainDB.transaction([this.objStore], "readwrite");
648     var objStore = transaction.objectStore(this.objStore);
649     var request = objStore.put(aValue, aKey);
650     request.onsuccess = function(event) {
651       success = true;
652       if (aCallback)
653         aCallback(success, event);
654     };
655     request.onerror = function(event) {
656       // Errors can be handled here.
657       if (aCallback)
658         aCallback(success, event);
659     };
660   },
661
662   unset: function(aKey, aCallback) {
663     if (!mainDB)
664       return;
665     var success = false;
666     var transaction = mainDB.transaction([this.objStore], "readwrite");
667     var request = transaction.objectStore(this.objStore).delete(aKey);
668     request.onsuccess = function(event) {
669       success = true;
670       if (aCallback)
671         aCallback(success, event);
672     };
673     request.onerror = function(event) {
674       // Errors can be handled here.
675       if (aCallback)
676         aCallback(success, event);
677     }
678   }
679 };
680
681 var gBMStore = {
682   objStore: "bookmarks",
683
684   getList: function(aCallback) {
685     if (!mainDB)
686       return;
687     var transaction = mainDB.transaction([this.objStore]);
688     var objStore = transaction.objectStore(this.objStore);
689     if (objStore.getAll) { // currently Mozilla-specific
690       objStore.getAll().onsuccess = function(event) {
691         aCallback(event.target.result);
692       };
693     }
694     else { // Use cursor (standard method).
695       var BMs = {};
696       objStore.openCursor().onsuccess = function(event) {
697         var cursor = event.target.result;
698         if (cursor) {
699           BMs[cursor.key] = cursor.value;
700           cursor.continue();
701         }
702         else {
703           aCallback(BMs);
704         }
705       };
706     }
707   },
708
709   get: function(aKey, aCallback) {
710     if (!mainDB)
711       return;
712     var transaction = mainDB.transaction([this.objStore]);
713     var request = transaction.objectStore(this.objStore).get(aKey);
714     request.onsuccess = function(event) {
715       aCallback(request.result, event);
716     };
717     request.onerror = function(event) {
718       // Errors can be handled here.
719       aCallback(undefined, event);
720     };
721   },
722
723   set: function(aKey, aValue, aCallback) {
724     if (!mainDB)
725       return;
726     var success = false;
727     var transaction = mainDB.transaction([this.objStore], "readwrite");
728     var objStore = transaction.objectStore(this.objStore);
729     var request = objStore.put(aValue, aKey);
730     request.onsuccess = function(event) {
731       success = true;
732       if (aCallback)
733         aCallback(success, event);
734     };
735     request.onerror = function(event) {
736       // Errors can be handled here.
737       if (aCallback)
738         aCallback(success, event);
739     };
740   },
741
742   unset: function(aKey, aCallback) {
743     if (!mainDB)
744       return;
745     var success = false;
746     var transaction = mainDB.transaction([this.objStore], "readwrite");
747     var request = transaction.objectStore(this.objStore).delete(aKey);
748     request.onsuccess = function(event) {
749       success = true;
750       if (aCallback)
751         aCallback(success, event);
752     };
753     request.onerror = function(event) {
754       // Errors can be handled here.
755       if (aCallback)
756         aCallback(success, event);
757     }
758   },
759
760   clear: function(aCallback) {
761     if (!mainDB)
762       return;
763     var success = false;
764     var transaction = mainDB.transaction([this.objStore], "readwrite");
765     var request = transaction.objectStore(this.objStore).clear();
766     request.onsuccess = function(event) {
767       success = true;
768       if (aCallback)
769         aCallback(success, event);
770     };
771     request.onerror = function(event) {
772       // Errors can be handled here.
773       if (aCallback)
774         aCallback(success, event);
775     }
776   }
777 };