68cdffe30e20cb902589518683a53182fefd3903
[mandelbrot.git] / content / mandelbrot.js
1 /* ***** BEGIN LICENSE BLOCK *****
2  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3  *
4  * The contents of this file are subject to the Mozilla Public License Version
5  * 1.1 (the "License"); you may not use this file except in compliance with
6  * the License. You may obtain a copy of the License at
7  * http://www.mozilla.org/MPL/
8  *
9  * Software distributed under the License is distributed on an "AS IS" basis,
10  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11  * for the specific language governing rights and limitations under the
12  * License.
13  *
14  * The Original Code is KaiRo.at Mandelbrot, XULRunner version.
15  *
16  * The Initial Developer of the Original Code is
17  * Robert Kaiser <kairo@kairo.at>.
18  * Portions created by the Initial Developer are Copyright (C) 2008-2011
19  * the Initial Developer. All Rights Reserved.
20  *
21  * Contributor(s):
22  *   Robert Kaiser <kairo@kairo.at>
23  *   prefiks (patch for some speedups)
24  *   Boris Zbarsky <bzbarsky@mit.edu> (use imageData for canvas interaction)
25  *
26  * Alternatively, the contents of this file may be used under the terms of
27  * either the GNU General Public License Version 2 or later (the "GPL"), or
28  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
29  * in which case the provisions of the GPL or the LGPL are applicable instead
30  * of those above. If you wish to allow use of your version of this file only
31  * under the terms of either the GPL or the LGPL, and not to allow others to
32  * use your version of this file under the terms of the MPL, indicate your
33  * decision by deleting the provisions above and replace them with the notice
34  * and other provisions required by the GPL or the LGPL. If you do not delete
35  * the provisions above, a recipient may use your version of this file under
36  * the terms of any one of the MPL, the GPL or the LGPL.
37  *
38  * ***** END LICENSE BLOCK ***** */
39
40 var gColorPalette = [];
41 var gPref = Components.classes["@mozilla.org/preferences-service;1"]
42                       .getService(Components.interfaces.nsIPrefService)
43                       .getBranch(null);
44 var gStartTime = 0;
45 var gMbrotBundle;
46 var gCurrentImageData;
47
48 function Startup() {
49   updateIterMenu();
50   updateAlgoMenu();
51   updatePaletteMenu();
52   gMbrotBundle = document.getElementById("mbrotBundle");
53   document.getElementById("statusLabel").value = gMbrotBundle.getString("statusEmpty");
54
55   let img = document.getElementById("mbrotImage");
56   img.addEventListener("mouseup", imgEvHandler, false);
57   img.addEventListener("mousedown", imgEvHandler, false);
58   img.addEventListener("mousemove", imgEvHandler, false);
59   img.addEventListener("touchstart", imgEvHandler, false);
60   img.addEventListener("touchend", imgEvHandler, false);
61   img.addEventListener("touchcancel", imgEvHandler, false);
62   img.addEventListener("touchleave", imgEvHandler, false);
63   img.addEventListener("touchmove", imgEvHandler, false);
64 }
65
66 function getAdjustPref(prefname) {
67   let value;
68   switch (prefname) {
69     case "image.width":
70     case "image.height":
71       value = 0;
72       try {
73         value = gPref.getIntPref("mandelbrot." + prefname);
74       }
75       catch (e) { }
76       if ((value < 10) || (value > 5000)) {
77         value = 300;
78         gPref.setIntPref("mandelbrot." + prefname, value);
79       }
80       return value;
81     case "last_image.Cr_*":
82       let Cr_min = -2.0;
83       let Cr_max = 1.0;
84       try {
85         Cr_min = parseFloat(gPref.getCharPref("mandelbrot.last_image.Cr_min"));
86         Cr_max = parseFloat(gPref.getCharPref("mandelbrot.last_image.Cr_max"));
87       }
88       catch (e) { }
89       if ((Cr_min < -3) || (Cr_min > 2) ||
90           (Cr_max < -3) || (Cr_max > 2) || (Cr_min >= Cr_max)) {
91         Cr_min = -2.0; Cr_max = 1.0;
92       }
93       gPref.setCharPref("mandelbrot.last_image.Cr_min", Cr_min);
94       gPref.setCharPref("mandelbrot.last_image.Cr_max", Cr_max);
95       return {Cr_min: Cr_min, Cr_max: Cr_max};
96     case "last_image.Ci_*":
97       let Ci_min = -1.5;
98       let Ci_max = 1.5;
99       try {
100         Ci_min = parseFloat(gPref.getCharPref("mandelbrot.last_image.Ci_min"));
101         Ci_max = parseFloat(gPref.getCharPref("mandelbrot.last_image.Ci_max"));
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       gPref.setCharPref("mandelbrot.last_image.Ci_min", Ci_min);
109       gPref.setCharPref("mandelbrot.last_image.Ci_max", Ci_max);
110       return {Ci_min: Ci_min, Ci_max: Ci_max};
111     case "iteration_max":
112       value = 500;
113       try {
114         value = gPref.getIntPref("mandelbrot." + prefname);
115       }
116       catch (e) {
117         setIter(value);
118       }
119       if (value < 10 || value > 10000) {
120         value = 500;
121         setIter(value);
122       }
123       return value;
124     case "use_algorithm":
125       value = "numeric";
126       try {
127         value = gPref.getCharPref("mandelbrot." + prefname);
128       }
129       catch (e) {
130         setAlgorithm(value);
131       }
132       return value;
133    case "color_palette":
134       value = "kairo";
135       try {
136         value = gPref.getCharPref("mandelbrot." + prefname);
137       }
138       catch(e) {
139         setPalette(value);
140       }
141       return value;
142    case "syncProportions":
143       value = true;
144       try {
145         value = gPref.getBoolPref("mandelbrot." + prefname);
146       }
147       catch(e) {
148         gPref.setBoolPref("mandelbrot." + prefname, value);
149       }
150       return value;
151     default:
152       return false;
153   }
154 }
155
156 function adjustCoordsAndDraw(aC_min, aC_max) {
157   let iWidth = getAdjustPref("image.width");
158   let iHeight = getAdjustPref("image.height");
159
160   // correct coordinates
161   if (aC_min.r < -2)
162     aC_min.r = -2;
163   if (aC_max.r > 2)
164     aC_max.r = 2;
165   if ((aC_min.r > 2) || (aC_max.r < -2) || (aC_min.r >= aC_max.r)) {
166     aC_min.r = -2.0; aC_max.r = 1.0;
167   }
168   if (aC_min.i < -2)
169     aC_min.i = -2;
170   if (aC_max.i > 2)
171     aC_max.i = 2;
172   if ((aC_min.i > 2) || (aC_max.i < -2) || (aC_min.i >= aC_max.i)) {
173     aC_min.i = -1.3; aC_max.i = 1.3;
174   }
175
176   let CWidth = aC_max.r - aC_min.r;
177   let CHeight = aC_max.i - aC_min.i;
178   let C_mid = new complex(aC_min.r + CWidth / 2, aC_min.i + CHeight / 2);
179
180   let CRatio = Math.max(CWidth / iWidth, CHeight / iHeight);
181
182   gPref.setCharPref("mandelbrot.last_image.Cr_min", C_mid.r - iWidth * CRatio / 2);
183   gPref.setCharPref("mandelbrot.last_image.Cr_max", C_mid.r + iWidth * CRatio / 2);
184   gPref.setCharPref("mandelbrot.last_image.Ci_min", C_mid.i - iHeight * CRatio / 2);
185   gPref.setCharPref("mandelbrot.last_image.Ci_max", C_mid.i + iHeight * CRatio / 2);
186
187   drawImage();
188 }
189
190 function drawImage() {
191   let canvas = document.getElementById("mbrotImage");
192   let context = canvas.getContext("2d");
193
194   document.getElementById("drawButton").hidden = true;
195
196   document.getElementById("statusLabel").value = gMbrotBundle.getString("statusDrawing");
197
198   let Cr_vals = getAdjustPref("last_image.Cr_*");
199   let Cr_min = Cr_vals.Cr_min;
200   let Cr_max = Cr_vals.Cr_max;
201
202   let Ci_vals = getAdjustPref("last_image.Ci_*");
203   let Ci_min = Ci_vals.Ci_min;
204   let Ci_max = Ci_vals.Ci_max;
205
206   let iterMax = getAdjustPref("iteration_max");
207   let algorithm = getAdjustPref("use_algorithm");
208
209   let iWidth = getAdjustPref("image.width");
210   let iHeight = getAdjustPref("image.height");
211
212   gCurrentImageData = {
213     C_min: new complex(Cr_min, Ci_min),
214     C_max: new complex(Cr_max, Ci_max),
215     iWidth: iWidth,
216     iHeight: iHeight,
217     iterMax: iterMax
218   };
219
220   canvas.width = iWidth;
221   canvas.height = iHeight;
222
223   context.fillStyle = "rgba(255, 255, 255, 127)";
224   context.fillRect(0, 0, canvas.width, canvas.height);
225
226   gStartTime = new Date();
227
228   drawLine(0, [Cr_min, Cr_max, Ci_min, Ci_max],
229               canvas, context, iterMax, algorithm);
230 }
231
232 function drawLine(line, dimensions, canvas, context, iterMax, algorithm) {
233   let Cr_min = dimensions[0];
234   let Cr_max = dimensions[1];
235   let Cr_scale = Cr_max - Cr_min;
236
237   let Ci_min = dimensions[2];
238   let Ci_max = dimensions[3];
239   let Ci_scale = Ci_max - Ci_min;
240
241   let lines = Math.min(canvas.height - line, 8);
242   let imageData = context.createImageData(canvas.width, lines);
243   let pixels = imageData.data;
244   let idx = 0;
245   for (var img_y = line; img_y < canvas.height && img_y < line+8; img_y++)
246     for (let img_x = 0; img_x < canvas.width; img_x++) {
247       let C = new complex(Cr_min + (img_x / canvas.width) * Cr_scale,
248                           Ci_min + (img_y / canvas.height) * Ci_scale);
249       let colors = drawPoint(context, img_x, img_y, C, iterMax, algorithm);
250       pixels[idx++] = colors[0];
251       pixels[idx++] = colors[1];
252       pixels[idx++] = colors[2];
253       pixels[idx++] = colors[3];
254     }
255   context.putImageData(imageData, 0, line);
256
257   if (img_y < canvas.height)
258     setTimeout(drawLine, 0, img_y, dimensions, canvas, context, iterMax, algorithm);
259   else if (gStartTime)
260     EndCalc();
261 }
262
263 function EndCalc() {
264   let endTime = new Date();
265   let timeUsed = (endTime.getTime() - gStartTime.getTime()) / 1000;
266   document.getElementById("statusLabel").value =
267       gMbrotBundle.getFormattedString("statusTime", [timeUsed.toFixed(3)]);
268   gStartTime = 0;
269 }
270
271 function complex(aReal, aImag) {
272   this.r = aReal;
273   this.i = aImag;
274 }
275 complex.prototype = {
276   square: function() {
277     return new complex(this.r * this.r - this.i * this.i,
278                        2 * this.r * this.i);
279   },
280   dist: function() {
281     return Math.sqrt(this.r * this.r + this.i * this.i);
282   },
283   add: function(aComplex) {
284     return new complex(this.r + aComplex.r, this.i + aComplex.i);
285   }
286 }
287
288 function mandelbrotValueOO (aC, aIterMax) {
289   // this would be nice code in general but it looks like JS objects are too heavy for normal use.
290   let Z = new complex(0.0, 0.0);
291   for (var iter = 0; iter < aIterMax; iter++) {
292     Z = Z.square().add(aC);
293     if (Z.r * Z.r + Z.i * Z.i > 256) { break; }
294   }
295   return iter;
296 }
297
298 function mandelbrotValueNumeric (aC, aIterMax) {
299   // optimized numeric code for fast calculation
300   let Cr = aC.r, Ci = aC.i;
301   let Zr = 0.0, Zi = 0.0;
302   let Zr2 = Zr * Zr, Zi2 = Zi * Zi;
303   for (var iter = 0; iter < aIterMax; iter++) {
304     Zi = 2 * Zr * Zi + Ci;
305     Zr = Zr2 - Zi2 + Cr;
306
307     Zr2 = Zr * Zr; Zi2 = Zi * Zi;
308     if (Zr2 + Zi2 > 256) { break; }
309   }
310   return iter;
311 }
312
313 function getColor(aIterValue, aIterMax) {
314   let standardizedValue = Math.round(aIterValue * 1024 / aIterMax);
315   if (gColorPalette && gColorPalette.length)
316     return gColorPalette[standardizedValue];
317
318   // fallback to simple b/w if for some reason we don't have a palette
319   if (aIterValue == aIterMax)
320     return [0, 0, 0, 255];
321   else
322     return [255, 255, 255, 255];
323 }
324
325 function getColorPalette(palName) {
326   var palette = [];
327   switch (palName) {
328     case 'bw':
329       for (let i = 0; i < 1024; i++) {
330         palette[i] = [255, 255, 255, 255];
331       }
332       palette[1024] = [0, 0, 0, 255];
333       break;
334     case 'kairo':
335       // outer areas
336       for (let i = 0; i < 32; i++) {
337         let cc1 = Math.floor(i * 127 / 31);
338         let cc2 = 170 - Math.floor(i * 43 / 31);
339         palette[i] = [cc1, cc2, cc1, 255];
340       }
341       // inner areas
342       for (let i = 0; i < 51; i++) {
343         let cc = Math.floor(i * 170 / 50);
344         palette[32 + i] = [cc, 0, (170-cc), 255];
345       }
346       // corona
347       for (let i = 0; i < 101; i++) {
348         let cc = Math.floor(i * 200 / 100);
349         palette[83 + i] = [255, cc, 0, 255];
350       }
351       // inner corona
352       for (let i = 0; i < 201; i++) {
353         let cc1 = 255 - Math.floor(i * 85 / 200);
354         let cc2 = 200 - Math.floor(i * 30 / 200);
355         let cc3 = Math.floor(i * 170 / 200);
356         palette[184 + i] = [cc1, cc2, cc3, 255];
357       }
358       for (let i = 0; i < 301; i++) {
359         let cc1 = 170 - Math.floor(i * 43 / 300);
360         let cc2 = 170 + Math.floor(i * 85 / 300);
361         palette[385 + i] = [cc1, cc1, cc2, 255];
362       }
363       for (let i = 0; i < 338; i++) {
364         let cc = 127 + Math.floor(i * 128 / 337);
365         palette[686 + i] = [cc, cc, 255, 255];
366       }
367       palette[1024] = [0, 0, 0, 255];
368       break;
369     case 'rainbow-linear1':
370       for (let i = 0; i < 256; i++) {
371         palette[i] = [i, 0, 0, 255];
372         palette[256 + i] = [255, i, 0, 255];
373         palette[512 + i] = [255 - i, 255, i, 255];
374         palette[768 + i] = [i, 255-i, 255, 255];
375       }
376       palette[1024] = [0, 0, 0, 255];
377       break;
378     case 'rainbow-squared1':
379       for (let i = 0; i < 34; i++) {
380         let cc = Math.floor(i * 255 / 33);
381         palette[i] = [cc, 0, 0, 255];
382       }
383       for (let i = 0; i < 137; i++) {
384         let cc = Math.floor(i * 255 / 136);
385         palette[34 + i] = [255, cc, 0, 255];
386       }
387       for (let i = 0; i < 307; i++) {
388         let cc = Math.floor(i * 255 / 306);
389         palette[171 + i] = [255 - cc, 255, cc, 255];
390       }
391       for (let i = 0; i < 546; i++) {
392         let cc = Math.floor(i * 255 / 545);
393         palette[478 + i] = [cc, 255 - cc, 255, 255];
394       }
395       palette[1024] = [0, 0, 0, 255];
396       break;
397     case 'rainbow-linear2':
398       for (let i = 0; i < 205; i++) {
399         let cc = Math.floor(i * 255 / 204);
400         palette[i] = [255, cc, 0, 255];
401         palette[204 + i] = [255 - cc, 255, 0, 255];
402         palette[409 + i] = [0, 255, cc, 255];
403         palette[614 + i] = [0, 255 - cc, 255, 255];
404         palette[819 + i] = [cc, 0, 255, 255];
405       }
406       palette[1024] = [0, 0, 0, 255];
407       break;
408     case 'rainbow-squared2':
409       for (let i = 0; i < 19; i++) {
410         let cc = Math.floor(i * 255 / 18);
411         palette[i] = [255, cc, 0, 255];
412       }
413       for (let i = 0; i < 74; i++) {
414         let cc = Math.floor(i * 255 / 73);
415         palette[19 + i] = [255 - cc, 255, 0, 255];
416       }
417       for (let i = 0; i < 168; i++) {
418         let cc = Math.floor(i * 255 / 167);
419         palette[93 + i] = [0, 255, cc, 255];
420       }
421       for (let i = 0; i < 298; i++) {
422         let cc = Math.floor(i * 255 / 297);
423         palette[261 + i] = [0, 255 - cc, 255, 255];
424       }
425       for (let i = 0; i < 465; i++) {
426         let cc = Math.floor(i * 255 / 464);
427         palette[559 + i] = [cc, 0, 255, 255];
428       }
429       palette[1024] = [0, 0, 0, 255];
430       break;
431   }
432   /*
433      'Standard-Palette (QB-Colors)
434      For i = 0 To 1024
435          xx = CInt(i * 500 / 1024 + 2)
436          If xx <= 15 Then clr = xx
437          If xx > 15 Then clr = CInt(Sqr((xx - 15 + 1) * 15 ^ 2 / 485))
438          If xx >= 500 Then clr = 0
439          palette(i) = QBColor(clr)
440      Next
441   */
442   return palette;
443 }
444
445 function drawPoint(context, img_x, img_y, C, iterMax, algorithm) {
446   var itVal;
447   switch (algorithm) {
448     case 'oo':
449       itVal = mandelbrotValueOO(C, iterMax);
450       break;
451     case 'numeric':
452     default:
453       itVal = mandelbrotValueNumeric(C, iterMax);
454       break;
455   }
456   return getColor(itVal, iterMax);
457 }
458
459 /***** pure UI functions *****/
460
461 var zoomstart;
462 var imgBackup;
463
464 let imgEvHandler = {
465   handleEvent: function(aEvent) {
466     let canvas = document.getElementById("mbrotImage");
467     let context = canvas.getContext("2d");
468     switch (aEvent.type) {
469       case 'mousedown':
470       case 'touchstart':
471         if (aEvent.button == 0) {
472           // left button - start dragzoom
473           zoomstart = {x: aEvent.clientX - canvas.offsetLeft,
474                        y: aEvent.clientY - canvas.offsetTop};
475           imgBackup = context.getImageData(0, 0, canvas.width, canvas.height);
476         }
477         break;
478       case 'mouseup':
479       case 'touchend':
480         if (aEvent.button == 0 && zoomstart) {
481           context.putImageData(imgBackup, 0, 0);
482           let zoomend = {x: aEvent.clientX - canvas.offsetLeft,
483                         y: aEvent.clientY - canvas.offsetTop};
484
485           // make sure zoomend is bigger than zoomstart
486           if ((zoomend.x == zoomstart.x) || (zoomend.y == zoomstart.y)) {
487             // cannot zoom what has no area, discard it
488             zoomstart = undefined;
489             return;
490           }
491           if (zoomend.x < zoomstart.x)
492             [zoomend.x, zoomstart.x] = [zoomstart.x, zoomend.x];
493           if (zoomend.y < zoomstart.y)
494             [zoomend.y, zoomstart.y] = [zoomstart.y, zoomend.y];
495
496           // determine new "coordinates"
497           let CWidth = gCurrentImageData.C_max.r - gCurrentImageData.C_min.r;
498           let CHeight = gCurrentImageData.C_max.i - gCurrentImageData.C_min.i;
499           let newC_min = new complex(
500               gCurrentImageData.C_min.r + zoomstart.x / gCurrentImageData.iWidth * CWidth,
501               gCurrentImageData.C_min.i + zoomstart.y / gCurrentImageData.iHeight * CHeight);
502           let newC_max = new complex(
503               gCurrentImageData.C_min.r + zoomend.x / gCurrentImageData.iWidth * CWidth,
504               gCurrentImageData.C_min.i + zoomend.y / gCurrentImageData.iHeight * CHeight);
505
506           adjustCoordsAndDraw(newC_min, newC_max);
507         }
508         zoomstart = undefined;
509         break;
510       case 'mousemove':
511       case 'touchmove':
512         if (aEvent.button == 0 && zoomstart) {
513           context.putImageData(imgBackup, 0, 0);
514           context.strokeStyle = "rgb(255,255,31)";
515           context.strokeRect(zoomstart.x, zoomstart.y,
516                              aEvent.clientX - canvas.offsetLeft - zoomstart.x,
517                              aEvent.clientY - canvas.offsetTop - zoomstart.y);
518         }
519       break;
520     }
521   }
522 };
523
524 function saveImage() {
525   const nsIFilePicker = Components.interfaces.nsIFilePicker;
526   let fp = null;
527   try {
528     fp = Components.classes["@mozilla.org/filepicker;1"]
529                    .createInstance(nsIFilePicker);
530   } catch (e) {}
531   if (!fp) return;
532   let promptString = gMbrotBundle.getString("savePrompt");
533   fp.init(window, promptString, nsIFilePicker.modeSave);
534   fp.appendFilter(gMbrotBundle.getString("pngFilterName"), "*.png");
535   fp.defaultString = "mandelbrot.png";
536
537   let fpResult = fp.show();
538   if (fpResult != nsIFilePicker.returnCancel) {
539     saveCanvas(document.getElementById("mbrotImage"), fp.file);
540   }
541 }
542
543 function exitMandelbrot() {
544   var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]
545                           .getService(Components.interfaces.nsIXULAppInfo);
546   if (appInfo.ID == "mandelbrot@kairo.at")
547     quitApp(false);
548   else
549     window.close();
550 }
551
552 function updateBookmarkMenu(aParent) {
553   document.getElementById("bookmarkSave").disabled =
554     (!document.getElementById("drawButton").hidden || (gStartTime > 0));
555
556   while (aParent.hasChildNodes() &&
557          aParent.lastChild.id != "bookmarkSeparator")
558     aParent.removeChild(aParent.lastChild);
559
560   let file = Components.classes["@mozilla.org/file/directory_service;1"]
561                        .getService(Components.interfaces.nsIProperties)
562                        .get("ProfD", Components.interfaces.nsIFile);
563   file.append("mandelbookmarks.sqlite");
564   if (file.exists()) {
565     let connection = Components.classes["@mozilla.org/storage/service;1"]
566                                .getService(Components.interfaces.mozIStorageService)
567                                .openDatabase(file);
568     try {
569       if (connection.tableExists("bookmarks")) {
570         let statement = connection.createStatement(
571             "SELECT name,ROWID FROM bookmarks ORDER BY ROWID ASC");
572         while (statement.executeStep()) {
573           let newItem = aParent.appendChild(document.createElement("menuitem"));
574           newItem.setAttribute("label", statement.getString(0));
575           newItem.setAttribute("bmRowID", statement.getString(1));
576         }
577         statement.reset();
578         statement.finalize();
579         return;
580       }
581     } finally {
582       connection.close();
583     }
584   }
585   // Create the "Nothing Available" Menu item and disable it.
586   let na = aParent.appendChild(document.createElement("menuitem"));
587   na.setAttribute("label", gMbrotBundle.getString("noBookmarks"));
588   na.setAttribute("disabled", "true");
589 }
590
591 function callBookmark(evtarget) {
592   if (evtarget.id == "bookmarkSave" || evtarget.id == "bookmarkSeparator")
593     return;
594   if (evtarget.id == "bookmarkOverview") {
595     adjustCoordsAndDraw(new complex(0,0), new complex(0,0));
596     return;
597   }
598
599   if (evtarget.getAttribute('bmRowID')) {
600     let iterMax = 0;
601     let C_min = null;
602     let C_max = null;
603
604     let file = Components.classes["@mozilla.org/file/directory_service;1"]
605                          .getService(Components.interfaces.nsIProperties)
606                          .get("ProfD", Components.interfaces.nsIFile);
607     file.append("mandelbookmarks.sqlite");
608     let connection = Components.classes["@mozilla.org/storage/service;1"]
609                                .getService(Components.interfaces.mozIStorageService)
610                                .openDatabase(file);
611     let statement = connection.createStatement(
612         "SELECT iteration_max,Cr_min,Cr_max,Ci_min,Ci_max FROM bookmarks WHERE ROWID=?1");
613     statement.bindStringParameter(0, evtarget.getAttribute('bmRowID'));
614     while (statement.executeStep()) {
615       iterMax = statement.getInt32(0);
616       C_min = new complex(statement.getDouble(1), statement.getDouble(3));
617       C_max = new complex(statement.getDouble(2), statement.getDouble(4));
618     }
619     statement.finalize();
620     connection.close();
621
622     if (iterMax && C_min && C_max) {
623       gPref.setIntPref("mandelbrot.iteration_max", iterMax);
624       adjustCoordsAndDraw(C_min, C_max);
625     }
626   }
627 }
628
629 function saveBookmark() {
630   // retrieve wanted bookmark name with a prompt
631   let prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
632                           .getService(Components.interfaces.nsIPromptService);
633   let input = {value: ""}; // empty default value
634   let ok = prompts.prompt(null, gMbrotBundle.getString("saveBookmarkTitle"), gMbrotBundle.getString("saveBookmarkLabel"), input, null, {});
635   // ok is true if OK is pressed, false if Cancel. input.value holds the value of the edit field if "OK" was pressed.
636   if (!ok || !input.value)
637     return
638
639   let bmName = input.value;
640
641   // Open or create the bookmarks database.
642   let file = Components.classes["@mozilla.org/file/directory_service;1"]
643                        .getService(Components.interfaces.nsIProperties)
644                        .get("ProfD", Components.interfaces.nsIFile);
645   file.append("mandelbookmarks.sqlite");
646   let connection = Components.classes["@mozilla.org/storage/service;1"]
647                              .getService(Components.interfaces.mozIStorageService)
648                              .openDatabase(file);
649   connection.beginTransaction();
650   if (!connection.tableExists("bookmarks"))
651     connection.createTable("bookmarks", "name TEXT, iteration_max INTEGER, Cr_min REAL, Cr_max REAL, Ci_min REAL, Ci_max REAL");
652   // NULL. The value is a NULL value.
653   // INTEGER. The value is a signed integer, stored in 1, 2, 3, 4, 6, or 8 bytes depending on the magnitude of the value.
654   // REAL. The value is a floating point value, stored as an 8-byte IEEE floating point number.
655   // TEXT. The value is a text string, stored using the database encoding (UTF-8, UTF-16BE or UTF-16-LE).
656
657   // Put value of the current image into the bookmarks table
658   let statement = connection.createStatement(
659       "INSERT INTO bookmarks (name,iteration_max,Cr_min,Cr_max,Ci_min,Ci_max) VALUES (?1,?2,?3,?4,?5,?6)");
660   statement.bindStringParameter(0, bmName);
661   statement.bindStringParameter(1, gCurrentImageData.iterMax);
662   statement.bindStringParameter(2, gCurrentImageData.C_min.r);
663   statement.bindStringParameter(3, gCurrentImageData.C_max.r);
664   statement.bindStringParameter(4, gCurrentImageData.C_min.i);
665   statement.bindStringParameter(5, gCurrentImageData.C_max.i);
666   statement.execute();
667   statement.finalize();
668   connection.commitTransaction();
669   connection.close();
670 }
671
672 function updateIterMenu() {
673   let currentIter = getAdjustPref("iteration_max");
674
675   let popup = document.getElementById("menu_iterPopup");
676   let item = popup.firstChild;
677   while (item) {
678     if (item.getAttribute("name") == "iter") {
679       if (item.getAttribute("value") == currentIter)
680         item.setAttribute("checked","true");
681       else
682         item.removeAttribute("checked");
683     }
684     item = item.nextSibling;
685   }
686 }
687
688 function setIter(aIter) {
689   gPref.setIntPref("mandelbrot.iteration_max", aIter);
690 }
691
692 function updatePaletteMenu() {
693   let currentPalette = getAdjustPref("color_palette");
694   if (!gColorPalette || !gColorPalette.length)
695     gColorPalette = getColorPalette(currentPalette);
696
697   let popup = document.getElementById("menu_palettePopup");
698   let item = popup.firstChild;
699   while (item) {
700     if (item.getAttribute("name") == "palette") {
701       if (item.getAttribute("value") == currentPalette)
702         item.setAttribute("checked", "true");
703       else
704         item.removeAttribute("checked");
705     }
706     item = item.nextSibling;
707   }
708 }
709
710 function setPalette(aPaletteID) {
711   gPref.setCharPref("mandelbrot.color_palette", aPaletteID);
712   gColorPalette = getColorPalette(aPaletteID);
713 }
714
715 function imgSettings() {
716   let anchor = null;
717   let position = "before_start";
718   if (document.getElementById("mandelbrotWindow").nodeName == "page") {
719     anchor = document.getElementById("mandelbrotToolbar");
720   }
721   else {
722     anchor = document.getElementById("mandelbrotMenubar");
723     position = "after_start";
724   }
725   document.getElementById("imgSettingsPanel").showPopup(anchor, position);
726 }
727
728 function updateDebugMenu() {
729   let scope = (document.getElementById("mandelbrotWindow").nodeName == "page") ? "content" : "chrome";
730   try {
731     // This throws in versions that don't have JaegerMonkey yet --> catch block
732     gPref.getBoolPref("javascript.options.methodjit." + scope);
733
734     // We have JaegerMonkey, i.e. two prefs for trace/method JIT
735     for each (let type in ["tracejit", "methodjit"]) {
736       let jitMenuItem = document.getElementById(type + "Enabled");
737       jitMenuItem.setAttribute("checked", gPref.getBoolPref("javascript.options." + type + "." + scope));
738     }
739   }
740   catch (e) {
741     // We have TraceMonkey only, i.e. one JIT pref, care only that is displayed
742     for each (let type in ["tracejit", "methodjit"])
743       document.getElementById(type + "Enabled").hidden = true;
744     let jitMenuItem = document.getElementById("jitEnabled");
745     jitMenuItem.hidden = false;
746     jitMenuItem.setAttribute("checked", gPref.getBoolPref("javascript.options.jit." + scope));
747   }
748 }
749
750 function toggleJITState(jitMenuItem, jittype) {
751   let scope = (document.getElementById("mandelbrotWindow").nodeName == "page") ? "content" : "chrome";
752   let jitpref = "javascript.options." + jittype + "jit." + scope;
753   let jitEnabled = !gPref.getBoolPref(jitpref);
754   gPref.setBoolPref(jitpref, jitEnabled)
755   jitMenuItem.setAttribute("checked", jitEnabled ? "true" : "false");
756 }
757
758 function updateAlgoMenu() {
759   let currentAlgo = getAdjustPref("use_algorithm");
760
761   let popup = document.getElementById("menu_algoPopup");
762   let item = popup.firstChild;
763   while (item) {
764     if (item.getAttribute("name") == "algorithm") {
765       if (item.getAttribute("value") == currentAlgo)
766         item.setAttribute("checked", "true");
767       else
768         item.removeAttribute("checked");
769     }
770     item = item.nextSibling;
771   }
772 }
773
774 function setAlgorithm(algoID) {
775   gPref.setCharPref("mandelbrot.use_algorithm", algoID);
776 }
777
778 function initImgSettings() {
779   // Get values from prefs.
780   for each (let coord in ["Cr", "Ci"]) {
781     let coord_vals = getAdjustPref("last_image." + coord + "_*");
782     document.getElementById("is_" + coord + "_min").value = coord_vals[coord + "_min"];
783     document.getElementById("is_" + coord + "_max").value = coord_vals[coord + "_max"];
784   }
785   for each (let dim in ["width", "height"]) {
786     document.getElementById("is_img_" + dim).value = getAdjustPref("image." + dim);
787   }
788   document.getElementById("is_syncProp").checked = getAdjustPref("syncProportions");
789
790   // Calculate scales.
791   recalcCoord("Cr", "scale");
792   recalcCoord("Ci", "scale");
793
794   // Clear the preview.
795   let canvas = document.getElementById("is_mbrotPreview");
796   let context = canvas.getContext("2d");
797   context.fillStyle = "rgba(255, 255, 255, 127)";
798   context.fillRect(0, 0, canvas.width, canvas.height);
799 }
800
801 function closeImgSettings() {
802   // Hide popup, which will automatically make a call to save values.
803   document.getElementById("imgSettingsPanel").hidePopup();
804 }
805
806 function saveImgSettings() {
807   // Get values to prefs.
808   for each (let coord in ["Cr_min", "Cr_max", "Ci_min", "Ci_max"]) {
809     gPref.setCharPref("mandelbrot.last_image." + coord,
810                       document.getElementById("is_" + coord).value);
811   }
812   for each (let dim in ["width", "height"]) {
813     gPref.setIntPref("mandelbrot.image." + dim,
814                      document.getElementById("is_img_" + dim).value);
815   }
816   gPref.setBoolPref("mandelbrot.syncProportions",
817                     document.getElementById("is_syncProp").checked);
818 }
819
820 function checkISValue(textbox, type) {
821   if (type == "coord") {
822     textbox.value = roundCoord(parseFloat(textbox.value));
823   }
824   else if (type == "dim") {
825     textbox.value = parseInt(textbox.value);
826   }
827 }
828
829 function drawPreview() {
830   let canvas = document.getElementById("is_mbrotPreview");
831   let context = canvas.getContext("2d");
832
833   if (document.getElementById("is_img_width").value /
834       document.getElementById("is_img_height").value
835         < 80 / 50) {
836     canvas.height = 50;
837     canvas.width = canvas.height *
838       document.getElementById("is_img_width").value /
839       document.getElementById("is_img_height").value;
840   }
841   else {
842     canvas.width = 80;
843     canvas.height = canvas.width *
844       document.getElementById("is_imgHeight").value /
845       document.getElementById("is_imgWidth").value;
846   }
847
848   let Cr_min = parseFloat(document.getElementById("is_Cr_min").value);
849   let Cr_max = parseFloat(document.getElementById("is_Cr_max").value);
850   if ((Cr_min < -2) || (Cr_min > 2) ||
851       (Cr_max < -2) || (Cr_max > 2) || (Cr_min >= Cr_max)) {
852     Cr_min = -2.0; Cr_max = 1.0;
853   }
854
855   let Ci_min = parseFloat(document.getElementById("is_Ci_min").value);
856   let Ci_max = parseFloat(document.getElementById("is_Ci_max").value);
857   if ((Ci_min < -2) || (Ci_min > 2) ||
858       (Ci_max < -2) || (Ci_max > 2) || (Ci_min >= Ci_max)) {
859     Ci_min = -2.0; Ci_max = 1.0;
860   }
861
862   let iterMax = getAdjustPref("iteration_max");
863   let algorithm = getAdjustPref("use_algorithm");
864
865   context.fillStyle = "rgba(255, 255, 255, 127)";
866   context.fillRect(0, 0, canvas.width, canvas.height);
867
868   let currentPalette = getAdjustPref("color_palette");
869   gColorPalette = getColorPalette(currentPalette);
870
871   drawLine(0, [Cr_min, Cr_max, Ci_min, Ci_max],
872               canvas, context, iterMax, algorithm);
873 }
874
875 function recalcCoord(coord, target) {
876   let othercoord = (coord == "Ci") ? "Cr" : "Ci";
877   let owndim = (coord == "Ci") ? "height" : "width";
878   let otherdim = (coord == "Ci") ? "width" : "height";
879   let myscale;
880   if (target == "scale") {
881     myscale =
882       parseFloat(document.getElementById("is_" + coord + "_max").value) -
883       parseFloat(document.getElementById("is_" + coord + "_min").value);
884     document.getElementById("is_" + coord + "_scale").value = roundCoord(myscale);
885   }
886   else if (target == 'max') {
887     let mymax =
888       parseFloat(document.getElementById("is_" + coord + "_min").value) +
889       parseFloat(document.getElementById("is_" + coord + "_scale").value);
890     document.getElementById("is_" + coord + "_max").value = roundCoord(mymax);
891     myscale = document.getElementById("is_" + coord + "_scale").value;
892   }
893   if (document.getElementById("is_syncProp").checked) {
894     let otherscale = myscale *
895       document.getElementById("is_img_" + otherdim).value /
896       document.getElementById("is_img_" + owndim).value;
897     document.getElementById("is_" + othercoord + "_scale").value = roundCoord(otherscale);
898     let othermax =
899       parseFloat(document.getElementById("is_" + othercoord + "_min").value) +
900       parseFloat(document.getElementById("is_" + othercoord + "_scale").value);
901     document.getElementById("is_" + othercoord + "_max").value = roundCoord(othermax);
902   }
903 }
904
905 function checkProportions() {
906   if (!document.getElementById("is_syncProp").checked) {
907     recalcCoord("Cr", "scale");
908   }
909 }
910
911 function roundCoord(floatval) {
912   // We should round to 10 decimals here or so
913   return parseFloat(floatval.toFixed(10));
914 }
915
916 /***** helper functions from external sources *****/
917
918 // function below is based on http://developer.mozilla.org/en/docs/Code_snippets:Canvas
919 // custom modifications:
920 //   - use "a"-prefix on function arguments
921 //   - take an nsILocalFile as aDestFile argument
922 //   - always do silent download
923 function saveCanvas(aCanvas, aDestFile) {
924   // create a data url from the canvas and then create URIs of the source and targets  
925   var io = Components.classes["@mozilla.org/network/io-service;1"]
926                      .getService(Components.interfaces.nsIIOService);
927   var source = io.newURI(aCanvas.toDataURL("image/png", ""), "UTF8", null);
928
929   // prepare to save the canvas data
930   var persist = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
931                           .createInstance(Components.interfaces.nsIWebBrowserPersist);
932
933   persist.persistFlags = Components.interfaces.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
934   persist.persistFlags |= Components.interfaces.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
935
936   // save the canvas data to the file
937   persist.saveURI(source, null, null, null, null, aDestFile);
938 }