diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000000000000000000000000000000000..6703b6159ad551d6ca598b60136a59fde7d009be
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,33 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+
+# Matches multiple files with brace expansion notation
+# Set default charset
+[*.{js,py}]
+charset = utf-8
+
+# 4 space indentation
+[*.py]
+indent_style = space
+indent_size = 4
+
+# Tab indentation (no size specified)
+[Makefile]
+indent_style = tab
+
+# Indentation override for all JS under lib directory
+[*.{js,ts,jsx,tsx}]
+indent_style = space
+indent_size = 2
+
+# Matches the exact files either package.json or .travis.yml
+[{package.json,.travis.yml}]
+indent_style = space
+indent_size = 2
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000000000000000000000000000000000..72446f434e370911e1eeed377dbd9e41b2860814
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+    "typescript.tsdk": "node_modules/typescript/lib"
+}
diff --git a/package-lock.json b/package-lock.json
index acb6434f92b33dab7143094b575faebb93e6edb6..f67e9bdb55d2609e87b3ed458e2ca8c23eb731f6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,7 @@
         "@types/react-dom": "^18.0.6",
         "react": "^18.2.0",
         "react-dom": "^18.2.0",
-        "react-scripts": "5.0.1",
+        "react-scripts": "^5.0.1",
         "typescript": "^4.7.4",
         "web-vitals": "^2.1.4"
       }
@@ -87,14 +87,6 @@
         "url": "https://opencollective.com/babel"
       }
     },
-    "node_modules/@babel/core/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
     "node_modules/@babel/eslint-parser": {
       "version": "7.18.9",
       "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.18.9.tgz",
@@ -140,14 +132,6 @@
         "node": ">=4.0"
       }
     },
-    "node_modules/@babel/eslint-parser/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
     "node_modules/@babel/generator": {
       "version": "7.18.12",
       "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz",
@@ -214,14 +198,6 @@
         "@babel/core": "^7.0.0"
       }
     },
-    "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
     "node_modules/@babel/helper-create-class-features-plugin": {
       "version": "7.18.9",
       "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.9.tgz",
@@ -273,14 +249,6 @@
         "@babel/core": "^7.4.0-0"
       }
     },
-    "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
     "node_modules/@babel/helper-environment-visitor": {
       "version": "7.18.9",
       "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
@@ -1544,14 +1512,6 @@
         "@babel/core": "^7.0.0-0"
       }
     },
-    "node_modules/@babel/plugin-transform-runtime/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
     "node_modules/@babel/plugin-transform-shorthand-properties": {
       "version": "7.18.6",
       "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz",
@@ -1756,14 +1716,6 @@
         "@babel/core": "^7.0.0-0"
       }
     },
-    "node_modules/@babel/preset-env/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
     "node_modules/@babel/preset-modules": {
       "version": "0.1.5",
       "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz",
@@ -3958,6 +3910,20 @@
         }
       }
     },
+    "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
+      "version": "7.3.7",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+      "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/@typescript-eslint/experimental-utils": {
       "version": "5.33.1",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.33.1.tgz",
@@ -4081,6 +4047,20 @@
         }
       }
     },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+      "version": "7.3.7",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+      "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/@typescript-eslint/utils": {
       "version": "5.33.1",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.33.1.tgz",
@@ -4893,14 +4873,6 @@
         "@babel/core": "^7.0.0-0"
       }
     },
-    "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
     "node_modules/babel-plugin-polyfill-corejs3": {
       "version": "0.5.3",
       "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.3.tgz",
@@ -5738,6 +5710,20 @@
         "webpack": "^5.0.0"
       }
     },
+    "node_modules/css-loader/node_modules/semver": {
+      "version": "7.3.7",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+      "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/css-minimizer-webpack-plugin": {
       "version": "3.4.1",
       "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz",
@@ -6901,14 +6887,6 @@
         "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
       }
     },
-    "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
     "node_modules/eslint-plugin-react": {
       "version": "7.30.1",
       "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.30.1.tgz",
@@ -6974,14 +6952,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/eslint-plugin-react/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
     "node_modules/eslint-plugin-testing-library": {
       "version": "5.6.0",
       "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.6.0.tgz",
@@ -7857,6 +7827,20 @@
         "url": "https://opencollective.com/webpack"
       }
     },
+    "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": {
+      "version": "7.3.7",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+      "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -8956,14 +8940,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/istanbul-lib-instrument/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
     "node_modules/istanbul-lib-report": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
@@ -10362,6 +10338,20 @@
         "node": ">=8"
       }
     },
+    "node_modules/jest-snapshot/node_modules/semver": {
+      "version": "7.3.7",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+      "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/jest-snapshot/node_modules/supports-color": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -11317,14 +11307,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/make-dir/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
     "node_modules/makeerror": {
       "version": "1.0.12",
       "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
@@ -12681,6 +12663,20 @@
         "webpack": "^5.0.0"
       }
     },
+    "node_modules/postcss-loader/node_modules/semver": {
+      "version": "7.3.7",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+      "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/postcss-logical": {
       "version": "5.0.4",
       "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz",
@@ -13825,6 +13821,20 @@
         }
       }
     },
+    "node_modules/react-scripts/node_modules/semver": {
+      "version": "7.3.7",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+      "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -14363,17 +14373,11 @@
       }
     },
     "node_modules/semver": {
-      "version": "7.3.7",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
-      "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
       "bin": {
         "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
       }
     },
     "node_modules/send": {
@@ -16591,13 +16595,6 @@
         "gensync": "^1.0.0-beta.2",
         "json5": "^2.2.1",
         "semver": "^6.3.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
-        }
       }
     },
     "@babel/eslint-parser": {
@@ -16628,11 +16625,6 @@
           "version": "4.3.0",
           "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
           "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
-        },
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
         }
       }
     },
@@ -16684,13 +16676,6 @@
         "@babel/helper-validator-option": "^7.18.6",
         "browserslist": "^4.20.2",
         "semver": "^6.3.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
-        }
       }
     },
     "@babel/helper-create-class-features-plugin": {
@@ -16727,13 +16712,6 @@
         "lodash.debounce": "^4.0.8",
         "resolve": "^1.14.2",
         "semver": "^6.1.2"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
-        }
       }
     },
     "@babel/helper-environment-visitor": {
@@ -17541,13 +17519,6 @@
         "babel-plugin-polyfill-corejs3": "^0.5.3",
         "babel-plugin-polyfill-regenerator": "^0.4.0",
         "semver": "^6.3.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
-        }
       }
     },
     "@babel/plugin-transform-shorthand-properties": {
@@ -17698,13 +17669,6 @@
         "babel-plugin-polyfill-regenerator": "^0.4.0",
         "core-js-compat": "^3.22.1",
         "semver": "^6.3.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
-        }
       }
     },
     "@babel/preset-modules": {
@@ -19264,6 +19228,16 @@
         "regexpp": "^3.2.0",
         "semver": "^7.3.7",
         "tsutils": "^3.21.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "7.3.7",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+          "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        }
       }
     },
     "@typescript-eslint/experimental-utils": {
@@ -19321,6 +19295,16 @@
         "is-glob": "^4.0.3",
         "semver": "^7.3.7",
         "tsutils": "^3.21.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "7.3.7",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+          "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        }
       }
     },
     "@typescript-eslint/utils": {
@@ -19935,13 +19919,6 @@
         "@babel/compat-data": "^7.17.7",
         "@babel/helper-define-polyfill-provider": "^0.3.2",
         "semver": "^6.1.1"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
-        }
       }
     },
     "babel-plugin-polyfill-corejs3": {
@@ -20565,6 +20542,16 @@
         "postcss-modules-values": "^4.0.0",
         "postcss-value-parser": "^4.2.0",
         "semver": "^7.3.5"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "7.3.7",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+          "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        }
       }
     },
     "css-minimizer-webpack-plugin": {
@@ -21499,13 +21486,6 @@
         "language-tags": "^1.0.5",
         "minimatch": "^3.1.2",
         "semver": "^6.3.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
-        }
       }
     },
     "eslint-plugin-react": {
@@ -21546,11 +21526,6 @@
             "path-parse": "^1.0.7",
             "supports-preserve-symlinks-flag": "^1.0.0"
           }
-        },
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
         }
       }
     },
@@ -22097,6 +22072,14 @@
             "ajv-keywords": "^3.4.1"
           }
         },
+        "semver": {
+          "version": "7.3.7",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+          "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
         "supports-color": {
           "version": "7.2.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -22857,13 +22840,6 @@
         "@istanbuljs/schema": "^0.1.2",
         "istanbul-lib-coverage": "^3.2.0",
         "semver": "^6.3.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
-        }
       }
     },
     "istanbul-lib-report": {
@@ -23884,6 +23860,14 @@
           "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
           "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
         },
+        "semver": {
+          "version": "7.3.7",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+          "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
         "supports-color": {
           "version": "7.2.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -24593,13 +24577,6 @@
       "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
       "requires": {
         "semver": "^6.0.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
-        }
       }
     },
     "makeerror": {
@@ -25461,6 +25438,16 @@
         "cosmiconfig": "^7.0.0",
         "klona": "^2.0.5",
         "semver": "^7.3.5"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "7.3.7",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+          "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        }
       }
     },
     "postcss-logical": {
@@ -26219,6 +26206,16 @@
         "webpack-dev-server": "^4.6.0",
         "webpack-manifest-plugin": "^4.0.2",
         "workbox-webpack-plugin": "^6.4.1"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "7.3.7",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+          "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        }
       }
     },
     "read-cache": {
@@ -26593,12 +26590,9 @@
       }
     },
     "semver": {
-      "version": "7.3.7",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
-      "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
-      "requires": {
-        "lru-cache": "^6.0.0"
-      }
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
     },
     "send": {
       "version": "0.18.0",
diff --git a/package.json b/package.json
index e70c3505596339c980dfdafcb6de677ce41dcb44..d8ee506e43f645dbe34d502768ec107a30c674c4 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
     "@types/react-dom": "^18.0.6",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
-    "react-scripts": "5.0.1",
+    "react-scripts": "^5.0.1",
     "typescript": "^4.7.4",
     "web-vitals": "^2.1.4"
   },
diff --git a/public/index.html b/public/index.html
index aa069f27cbd9d53394428171c3989fd03db73c76..4d0ece0cc48e94e727ff218e14516e2bab3e4352 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,21 +1,21 @@
 <!DOCTYPE html>
 <html lang="en">
-  <head>
-    <meta charset="utf-8" />
-    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
-    <meta name="theme-color" content="#000000" />
-    <meta
-      name="description"
-      content="Web site created using create-react-app"
-    />
-    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
-    <!--
+
+<head>
+  <meta charset="utf-8" />
+  <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+  <meta name="viewport" content="width=device-width, initial-scale=1" />
+  <meta name="theme-color" content="#000000" />
+  <meta name="description" content="Web site created using create-react-app" />
+  <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+  <!--
       manifest.json provides metadata used when your web app is installed on a
       user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
     -->
-    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
-    <!--
+  <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+  <link rel='stylesheet' href='https://aladin.u-strasbg.fr/AladinLite/api/v2/latest/aladin.min.css' />
+
+  <!--
       Notice the use of %PUBLIC_URL% in the tags above.
       It will be replaced with the URL of the `public` folder during the build.
       Only files inside the `public` folder can be referenced from the HTML.
@@ -24,12 +24,13 @@
       work correctly both with client-side routing and a non-root public URL.
       Learn how to configure a non-root public URL by running `npm run build`.
     -->
-    <title>React App</title>
-  </head>
-  <body>
-    <noscript>You need to enable JavaScript to run this app.</noscript>
-    <div id="root"></div>
-    <!--
+  <title>React App</title>
+</head>
+
+<body>
+  <noscript>You need to enable JavaScript to run this app.</noscript>
+  <div id="root"></div>
+  <!--
       This HTML file is a template.
       If you open it directly in the browser, you will see an empty page.
 
@@ -39,5 +40,9 @@
       To begin the development, run `npm start` or `yarn start`.
       To create a production bundle, use `npm run build` or `yarn build`.
     -->
-  </body>
+  <script type="text/javascript" src="https://code.jquery.com/jquery-1.12.1.min.js" charset="utf-8"></script>
+  <script type="text/javascript" src="https://aladin.u-strasbg.fr/AladinLite/api/v2/latest/aladin.min.js"
+    charset="utf-8"></script>
+</body>
+
 </html>
diff --git a/reference/aladin.js b/reference/aladin.js
new file mode 100644
index 0000000000000000000000000000000000000000..b6335496765b79e9459166779ec1e4c0a4568012
--- /dev/null
+++ b/reference/aladin.js
@@ -0,0 +1,14190 @@
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+// cds namespace
+
+var cds = cds || {};
+
+var A = A || {};
+/*
+    json2.js
+    2012-10-08
+
+    Public Domain.
+
+    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+
+    See http://www.JSON.org/js.html
+
+
+    This code should be minified before deployment.
+    See http://javascript.crockford.com/jsmin.html
+
+    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
+    NOT CONTROL.
+
+
+    This file creates a global JSON object containing two methods: stringify
+    and parse.
+
+        JSON.stringify(value, replacer, space)
+            value       any JavaScript value, usually an object or array.
+
+            replacer    an optional parameter that determines how object
+                        values are stringified for objects. It can be a
+                        function or an array of strings.
+
+            space       an optional parameter that specifies the indentation
+                        of nested structures. If it is omitted, the text will
+                        be packed without extra whitespace. If it is a number,
+                        it will specify the number of spaces to indent at each
+                        level. If it is a string (such as '\t' or '&nbsp;'),
+                        it contains the characters used to indent at each level.
+
+            This method produces a JSON text from a JavaScript value.
+
+            When an object value is found, if the object contains a toJSON
+            method, its toJSON method will be called and the result will be
+            stringified. A toJSON method does not serialize: it returns the
+            value represented by the name/value pair that should be serialized,
+            or undefined if nothing should be serialized. The toJSON method
+            will be passed the key associated with the value, and this will be
+            bound to the value
+
+            For example, this would serialize Dates as ISO strings.
+
+                Date.prototype.toJSON = function (key) {
+                    function f(n) {
+                        // Format integers to have at least two digits.
+                        return n < 10 ? '0' + n : n;
+                    }
+
+                    return this.getUTCFullYear()   + '-' +
+                         f(this.getUTCMonth() + 1) + '-' +
+                         f(this.getUTCDate())      + 'T' +
+                         f(this.getUTCHours())     + ':' +
+                         f(this.getUTCMinutes())   + ':' +
+                         f(this.getUTCSeconds())   + 'Z';
+                };
+
+            You can provide an optional replacer method. It will be passed the
+            key and value of each member, with this bound to the containing
+            object. The value that is returned from your method will be
+            serialized. If your method returns undefined, then the member will
+            be excluded from the serialization.
+
+            If the replacer parameter is an array of strings, then it will be
+            used to select the members to be serialized. It filters the results
+            such that only members with keys listed in the replacer array are
+            stringified.
+
+            Values that do not have JSON representations, such as undefined or
+            functions, will not be serialized. Such values in objects will be
+            dropped; in arrays they will be replaced with null. You can use
+            a replacer function to replace those with JSON values.
+            JSON.stringify(undefined) returns undefined.
+
+            The optional space parameter produces a stringification of the
+            value that is filled with line breaks and indentation to make it
+            easier to read.
+
+            If the space parameter is a non-empty string, then that string will
+            be used for indentation. If the space parameter is a number, then
+            the indentation will be that many spaces.
+
+            Example:
+
+            text = JSON.stringify(['e', {pluribus: 'unum'}]);
+            // text is '["e",{"pluribus":"unum"}]'
+
+
+            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
+            // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
+
+            text = JSON.stringify([new Date()], function (key, value) {
+                return this[key] instanceof Date ?
+                    'Date(' + this[key] + ')' : value;
+            });
+            // text is '["Date(---current time---)"]'
+
+
+        JSON.parse(text, reviver)
+            This method parses a JSON text to produce an object or array.
+            It can throw a SyntaxError exception.
+
+            The optional reviver parameter is a function that can filter and
+            transform the results. It receives each of the keys and values,
+            and its return value is used instead of the original value.
+            If it returns what it received, then the structure is not modified.
+            If it returns undefined then the member is deleted.
+
+            Example:
+
+            // Parse the text. Values that look like ISO date strings will
+            // be converted to Date objects.
+
+            myData = JSON.parse(text, function (key, value) {
+                var a;
+                if (typeof value === 'string') {
+                    a =
+/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
+                    if (a) {
+                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+                            +a[5], +a[6]));
+                    }
+                }
+                return value;
+            });
+
+            myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
+                var d;
+                if (typeof value === 'string' &&
+                        value.slice(0, 5) === 'Date(' &&
+                        value.slice(-1) === ')') {
+                    d = new Date(value.slice(5, -1));
+                    if (d) {
+                        return d;
+                    }
+                }
+                return value;
+            });
+
+
+    This is a reference implementation. You are free to copy, modify, or
+    redistribute.
+*/
+
+/*jslint evil: true, regexp: true */
+
+/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
+    call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
+    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
+    lastIndex, length, parse, prototype, push, replace, slice, stringify,
+    test, toJSON, toString, valueOf
+*/
+
+
+// Create a JSON object only if one does not already exist. We create the
+// methods in a closure to avoid creating global variables.
+
+if (typeof JSON !== 'object') {
+    JSON = {};
+}
+
+(function () {
+    'use strict';
+
+    function f(n) {
+        // Format integers to have at least two digits.
+        return n < 10 ? '0' + n : n;
+    }
+
+    if (typeof Date.prototype.toJSON !== 'function') {
+
+        Date.prototype.toJSON = function (key) {
+
+            return isFinite(this.valueOf())
+                ? this.getUTCFullYear()     + '-' +
+                    f(this.getUTCMonth() + 1) + '-' +
+                    f(this.getUTCDate())      + 'T' +
+                    f(this.getUTCHours())     + ':' +
+                    f(this.getUTCMinutes())   + ':' +
+                    f(this.getUTCSeconds())   + 'Z'
+                : null;
+        };
+
+        String.prototype.toJSON      =
+            Number.prototype.toJSON  =
+            Boolean.prototype.toJSON = function (key) {
+                return this.valueOf();
+            };
+    }
+
+    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+        escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+        gap,
+        indent,
+        meta = {    // table of character substitutions
+            '\b': '\\b',
+            '\t': '\\t',
+            '\n': '\\n',
+            '\f': '\\f',
+            '\r': '\\r',
+            '"' : '\\"',
+            '\\': '\\\\'
+        },
+        rep;
+
+
+    function quote(string) {
+
+// If the string contains no control characters, no quote characters, and no
+// backslash characters, then we can safely slap some quotes around it.
+// Otherwise we must also replace the offending characters with safe escape
+// sequences.
+
+        escapable.lastIndex = 0;
+        return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
+            var c = meta[a];
+            return typeof c === 'string'
+                ? c
+                : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+        }) + '"' : '"' + string + '"';
+    }
+
+
+    function str(key, holder) {
+
+// Produce a string from holder[key].
+
+        var i,          // The loop counter.
+            k,          // The member key.
+            v,          // The member value.
+            length,
+            mind = gap,
+            partial,
+            value = holder[key];
+
+// If the value has a toJSON method, call it to obtain a replacement value.
+
+        if (value && typeof value === 'object' &&
+                typeof value.toJSON === 'function') {
+            value = value.toJSON(key);
+        }
+
+// If we were called with a replacer function, then call the replacer to
+// obtain a replacement value.
+
+        if (typeof rep === 'function') {
+            value = rep.call(holder, key, value);
+        }
+
+// What happens next depends on the value's type.
+
+        switch (typeof value) {
+        case 'string':
+            return quote(value);
+
+        case 'number':
+
+// JSON numbers must be finite. Encode non-finite numbers as null.
+
+            return isFinite(value) ? String(value) : 'null';
+
+        case 'boolean':
+        case 'null':
+
+// If the value is a boolean or null, convert it to a string. Note:
+// typeof null does not produce 'null'. The case is included here in
+// the remote chance that this gets fixed someday.
+
+            return String(value);
+
+// If the type is 'object', we might be dealing with an object or an array or
+// null.
+
+        case 'object':
+
+// Due to a specification blunder in ECMAScript, typeof null is 'object',
+// so watch out for that case.
+
+            if (!value) {
+                return 'null';
+            }
+
+// Make an array to hold the partial results of stringifying this object value.
+
+            gap += indent;
+            partial = [];
+
+// Is the value an array?
+
+            if (Object.prototype.toString.apply(value) === '[object Array]') {
+
+// The value is an array. Stringify every element. Use null as a placeholder
+// for non-JSON values.
+
+                length = value.length;
+                for (i = 0; i < length; i += 1) {
+                    partial[i] = str(i, value) || 'null';
+                }
+
+// Join all of the elements together, separated with commas, and wrap them in
+// brackets.
+
+                v = partial.length === 0
+                    ? '[]'
+                    : gap
+                    ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
+                    : '[' + partial.join(',') + ']';
+                gap = mind;
+                return v;
+            }
+
+// If the replacer is an array, use it to select the members to be stringified.
+
+            if (rep && typeof rep === 'object') {
+                length = rep.length;
+                for (i = 0; i < length; i += 1) {
+                    if (typeof rep[i] === 'string') {
+                        k = rep[i];
+                        v = str(k, value);
+                        if (v) {
+                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
+                        }
+                    }
+                }
+            } else {
+
+// Otherwise, iterate through all of the keys in the object.
+
+                for (k in value) {
+                    if (Object.prototype.hasOwnProperty.call(value, k)) {
+                        v = str(k, value);
+                        if (v) {
+                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
+                        }
+                    }
+                }
+            }
+
+// Join all of the member texts together, separated with commas,
+// and wrap them in braces.
+
+            v = partial.length === 0
+                ? '{}'
+                : gap
+                ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
+                : '{' + partial.join(',') + '}';
+            gap = mind;
+            return v;
+        }
+    }
+
+// If the JSON object does not yet have a stringify method, give it one.
+
+    if (typeof JSON.stringify !== 'function') {
+        JSON.stringify = function (value, replacer, space) {
+
+// The stringify method takes a value and an optional replacer, and an optional
+// space parameter, and returns a JSON text. The replacer can be a function
+// that can replace values, or an array of strings that will select the keys.
+// A default replacer method can be provided. Use of the space parameter can
+// produce text that is more easily readable.
+
+            var i;
+            gap = '';
+            indent = '';
+
+// If the space parameter is a number, make an indent string containing that
+// many spaces.
+
+            if (typeof space === 'number') {
+                for (i = 0; i < space; i += 1) {
+                    indent += ' ';
+                }
+
+// If the space parameter is a string, it will be used as the indent string.
+
+            } else if (typeof space === 'string') {
+                indent = space;
+            }
+
+// If there is a replacer, it must be a function or an array.
+// Otherwise, throw an error.
+
+            rep = replacer;
+            if (replacer && typeof replacer !== 'function' &&
+                    (typeof replacer !== 'object' ||
+                    typeof replacer.length !== 'number')) {
+                throw new Error('JSON.stringify');
+            }
+
+// Make a fake root object containing our value under the key of ''.
+// Return the result of stringifying the value.
+
+            return str('', {'': value});
+        };
+    }
+
+
+// If the JSON object does not yet have a parse method, give it one.
+
+    if (typeof JSON.parse !== 'function') {
+        JSON.parse = function (text, reviver) {
+
+// The parse method takes a text and an optional reviver function, and returns
+// a JavaScript value if the text is a valid JSON text.
+
+            var j;
+
+            function walk(holder, key) {
+
+// The walk method is used to recursively walk the resulting structure so
+// that modifications can be made.
+
+                var k, v, value = holder[key];
+                if (value && typeof value === 'object') {
+                    for (k in value) {
+                        if (Object.prototype.hasOwnProperty.call(value, k)) {
+                            v = walk(value, k);
+                            if (v !== undefined) {
+                                value[k] = v;
+                            } else {
+                                delete value[k];
+                            }
+                        }
+                    }
+                }
+                return reviver.call(holder, key, value);
+            }
+
+
+// Parsing happens in four stages. In the first stage, we replace certain
+// Unicode characters with escape sequences. JavaScript handles many characters
+// incorrectly, either silently deleting them, or treating them as line endings.
+
+            text = String(text);
+            cx.lastIndex = 0;
+            if (cx.test(text)) {
+                text = text.replace(cx, function (a) {
+                    return '\\u' +
+                        ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+                });
+            }
+
+// In the second stage, we run the text against regular expressions that look
+// for non-JSON patterns. We are especially concerned with '()' and 'new'
+// because they can cause invocation, and '=' because it can cause mutation.
+// But just to be safe, we want to reject all unexpected forms.
+
+// We split the second stage into 4 regexp operations in order to work around
+// crippling inefficiencies in IE's and Safari's regexp engines. First we
+// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
+// replace all simple value tokens with ']' characters. Third, we delete all
+// open brackets that follow a colon or comma or that begin the text. Finally,
+// we look to see that the remaining characters are only whitespace or ']' or
+// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
+
+            if (/^[\],:{}\s]*$/
+                    .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
+                        .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
+                        .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+
+// In the third stage we use the eval function to compile the text into a
+// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
+// in JavaScript: it can begin a block or an object literal. We wrap the text
+// in parens to eliminate the ambiguity.
+
+                j = eval('(' + text + ')');
+
+// In the optional fourth stage, we recursively walk the new structure, passing
+// each name/value pair to a reviver function for possible transformation.
+
+                return typeof reviver === 'function'
+                    ? walk({'': j}, '')
+                    : j;
+            }
+
+// If the text is not JSON parseable, then a SyntaxError is thrown.
+
+            throw new SyntaxError('JSON.parse');
+        };
+    }
+}());// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+// log
+Logger = {};
+
+Logger.log = function(action, params) {
+    try {
+        var logUrl = "//alasky.unistra.fr/cgi/AladinLiteLogger/log.py";
+        var paramStr = "";
+        if (params) {
+            paramStr = JSON.stringify(params);
+        }
+
+        $.ajax({
+            url: logUrl,
+            data: {"action": action, "params": paramStr, "pageUrl": window.location.href, "referer": document.referrer ? document.referrer : ""},
+            method: 'GET',
+            dataType: 'json' // as alasky supports CORS, we do not need JSONP any longer
+        });
+
+    }
+    catch(e) {
+        window.console && console.log('Exception: ' + e);
+    }
+
+};
+/*!
+ * jQuery Mousewheel 3.1.13
+ *
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ */
+
+(function (factory) {
+    if ( typeof define === 'function' && define.amd ) {
+        // AMD. Register as an anonymous module.
+        define(['jquery'], factory);
+    } else if (typeof exports === 'object') {
+        // Node/CommonJS style for Browserify
+        module.exports = factory;
+    } else {
+        // Browser globals
+        factory(jQuery);
+    }
+}(function ($) {
+
+    var toFix  = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'],
+        toBind = ( 'onwheel' in document || document.documentMode >= 9 ) ?
+                    ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'],
+        slice  = Array.prototype.slice,
+        nullLowestDeltaTimeout, lowestDelta;
+
+    if ( $.event.fixHooks ) {
+        for ( var i = toFix.length; i; ) {
+            $.event.fixHooks[ toFix[--i] ] = $.event.mouseHooks;
+        }
+    }
+
+    var special = $.event.special.mousewheel = {
+        version: '3.1.12',
+
+        setup: function() {
+            if ( this.addEventListener ) {
+                for ( var i = toBind.length; i; ) {
+                    this.addEventListener( toBind[--i], handler, false );
+                }
+            } else {
+                this.onmousewheel = handler;
+            }
+            // Store the line height and page height for this particular element
+            $.data(this, 'mousewheel-line-height', special.getLineHeight(this));
+            $.data(this, 'mousewheel-page-height', special.getPageHeight(this));
+        },
+
+        teardown: function() {
+            if ( this.removeEventListener ) {
+                for ( var i = toBind.length; i; ) {
+                    this.removeEventListener( toBind[--i], handler, false );
+                }
+            } else {
+                this.onmousewheel = null;
+            }
+            // Clean up the data we added to the element
+            $.removeData(this, 'mousewheel-line-height');
+            $.removeData(this, 'mousewheel-page-height');
+        },
+
+        getLineHeight: function(elem) {
+            var $elem = $(elem),
+                $parent = $elem['offsetParent' in $.fn ? 'offsetParent' : 'parent']();
+            if (!$parent.length) {
+                $parent = $('body');
+            }
+            return parseInt($parent.css('fontSize'), 10) || parseInt($elem.css('fontSize'), 10) || 16;
+        },
+
+        getPageHeight: function(elem) {
+            return $(elem).height();
+        },
+
+        settings: {
+            adjustOldDeltas: true, // see shouldAdjustOldDeltas() below
+            normalizeOffset: true  // calls getBoundingClientRect for each event
+        }
+    };
+
+    $.fn.extend({
+        mousewheel: function(fn) {
+            return fn ? this.bind('mousewheel', fn) : this.trigger('mousewheel');
+        },
+
+        unmousewheel: function(fn) {
+            return this.unbind('mousewheel', fn);
+        }
+    });
+
+
+    function handler(event) {
+        var orgEvent   = event || window.event,
+            args       = slice.call(arguments, 1),
+            delta      = 0,
+            deltaX     = 0,
+            deltaY     = 0,
+            absDelta   = 0,
+            offsetX    = 0,
+            offsetY    = 0;
+        event = $.event.fix(orgEvent);
+        event.type = 'mousewheel';
+
+        // Old school scrollwheel delta
+        if ( 'detail'      in orgEvent ) { deltaY = orgEvent.detail * -1;      }
+        if ( 'wheelDelta'  in orgEvent ) { deltaY = orgEvent.wheelDelta;       }
+        if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY;      }
+        if ( 'wheelDeltaX' in orgEvent ) { deltaX = orgEvent.wheelDeltaX * -1; }
+
+        // Firefox < 17 horizontal scrolling related to DOMMouseScroll event
+        if ( 'axis' in orgEvent && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) {
+            deltaX = deltaY * -1;
+            deltaY = 0;
+        }
+
+        // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy
+        delta = deltaY === 0 ? deltaX : deltaY;
+
+        // New school wheel delta (wheel event)
+        if ( 'deltaY' in orgEvent ) {
+            deltaY = orgEvent.deltaY * -1;
+            delta  = deltaY;
+        }
+        if ( 'deltaX' in orgEvent ) {
+            deltaX = orgEvent.deltaX;
+            if ( deltaY === 0 ) { delta  = deltaX * -1; }
+        }
+
+        // No change actually happened, no reason to go any further
+        if ( deltaY === 0 && deltaX === 0 ) { return; }
+
+        // Need to convert lines and pages to pixels if we aren't already in pixels
+        // There are three delta modes:
+        //   * deltaMode 0 is by pixels, nothing to do
+        //   * deltaMode 1 is by lines
+        //   * deltaMode 2 is by pages
+        if ( orgEvent.deltaMode === 1 ) {
+            var lineHeight = $.data(this, 'mousewheel-line-height');
+            delta  *= lineHeight;
+            deltaY *= lineHeight;
+            deltaX *= lineHeight;
+        } else if ( orgEvent.deltaMode === 2 ) {
+            var pageHeight = $.data(this, 'mousewheel-page-height');
+            delta  *= pageHeight;
+            deltaY *= pageHeight;
+            deltaX *= pageHeight;
+        }
+
+        // Store lowest absolute delta to normalize the delta values
+        absDelta = Math.max( Math.abs(deltaY), Math.abs(deltaX) );
+
+        if ( !lowestDelta || absDelta < lowestDelta ) {
+            lowestDelta = absDelta;
+
+            // Adjust older deltas if necessary
+            if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) {
+                lowestDelta /= 40;
+            }
+        }
+
+        // Adjust older deltas if necessary
+        if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) {
+            // Divide all the things by 40!
+            delta  /= 40;
+            deltaX /= 40;
+            deltaY /= 40;
+        }
+
+        // Get a whole, normalized value for the deltas
+        delta  = Math[ delta  >= 1 ? 'floor' : 'ceil' ](delta  / lowestDelta);
+        deltaX = Math[ deltaX >= 1 ? 'floor' : 'ceil' ](deltaX / lowestDelta);
+        deltaY = Math[ deltaY >= 1 ? 'floor' : 'ceil' ](deltaY / lowestDelta);
+
+        // Normalise offsetX and offsetY properties
+        if ( special.settings.normalizeOffset && this.getBoundingClientRect ) {
+            var boundingRect = this.getBoundingClientRect();
+            offsetX = event.clientX - boundingRect.left;
+            offsetY = event.clientY - boundingRect.top;
+        }
+
+        // Add information to the event object
+        event.deltaX = deltaX;
+        event.deltaY = deltaY;
+        event.deltaFactor = lowestDelta;
+        event.offsetX = offsetX;
+        event.offsetY = offsetY;
+        // Go ahead and set deltaMode to 0 since we converted to pixels
+        // Although this is a little odd since we overwrite the deltaX/Y
+        // properties with normalized deltas.
+        event.deltaMode = 0;
+
+        // Add event and delta to the front of the arguments
+        args.unshift(event, delta, deltaX, deltaY);
+
+        // Clearout lowestDelta after sometime to better
+        // handle multiple device types that give different
+        // a different lowestDelta
+        // Ex: trackpad = 3 and mouse wheel = 120
+        if (nullLowestDeltaTimeout) { clearTimeout(nullLowestDeltaTimeout); }
+        nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200);
+
+        return ($.event.dispatch || $.event.handle).apply(this, args);
+    }
+
+    function nullLowestDelta() {
+        lowestDelta = null;
+    }
+
+    function shouldAdjustOldDeltas(orgEvent, absDelta) {
+        // If this is an older event and the delta is divisable by 120,
+        // then we are assuming that the browser is treating this as an
+        // older mouse wheel event and that we should divide the deltas
+        // by 40 to try and get a more usable deltaFactor.
+        // Side note, this actually impacts the reported scroll distance
+        // in older browsers and can cause scrolling to be slower than native.
+        // Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false.
+        return special.settings.adjustOldDeltas && orgEvent.type === 'mousewheel' && absDelta % 120 === 0;
+    }
+
+}));
+// requestAnimationFrame() shim by Paul Irish
+// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
+window.requestAnimFrame = (function() {
+	return  window.requestAnimationFrame       ||
+			window.webkitRequestAnimationFrame ||
+			window.mozRequestAnimationFrame    ||
+			window.oRequestAnimationFrame      ||
+			window.msRequestAnimationFrame     ||
+			function(/* function */ callback, /* DOMElement */ element){
+				window.setTimeout(callback, 1000 / 60);
+			};
+})();// stats.js r6 - http://github.com/mrdoob/stats.js
+var Stats=function(){function s(a,g,d){var f,c,e;for(c=0;c<30;c++)for(f=0;f<73;f++)e=(f+c*74)*4,a[e]=a[e+4],a[e+1]=a[e+5],a[e+2]=a[e+6];for(c=0;c<30;c++)e=(73+c*74)*4,c<g?(a[e]=b[d].bg.r,a[e+1]=b[d].bg.g,a[e+2]=b[d].bg.b):(a[e]=b[d].fg.r,a[e+1]=b[d].fg.g,a[e+2]=b[d].fg.b)}var r=0,t=2,g,u=0,j=(new Date).getTime(),F=j,v=j,l=0,w=1E3,x=0,k,d,a,m,y,n=0,z=1E3,A=0,f,c,o,B,p=0,C=1E3,D=0,h,i,q,E,b={fps:{bg:{r:16,g:16,b:48},fg:{r:0,g:255,b:255}},ms:{bg:{r:16,g:48,b:16},fg:{r:0,g:255,b:0}},mb:{bg:{r:48,g:16,
+b:26},fg:{r:255,g:0,b:128}}};g=document.createElement("div");g.style.cursor="pointer";g.style.width="80px";g.style.opacity="0.9";g.style.zIndex="10001";g.addEventListener("click",function(){r++;r==t&&(r=0);k.style.display="none";f.style.display="none";h.style.display="none";switch(r){case 0:k.style.display="block";break;case 1:f.style.display="block";break;case 2:h.style.display="block"}},!1);k=document.createElement("div");k.style.backgroundColor="rgb("+Math.floor(b.fps.bg.r/2)+","+Math.floor(b.fps.bg.g/
+2)+","+Math.floor(b.fps.bg.b/2)+")";k.style.padding="2px 0px 3px 0px";g.appendChild(k);d=document.createElement("div");d.style.fontFamily="Helvetica, Arial, sans-serif";d.style.textAlign="left";d.style.fontSize="9px";d.style.color="rgb("+b.fps.fg.r+","+b.fps.fg.g+","+b.fps.fg.b+")";d.style.margin="0px 0px 1px 3px";d.innerHTML='<span style="font-weight:bold">FPS</span>';k.appendChild(d);a=document.createElement("canvas");a.width=74;a.height=30;a.style.display="block";a.style.marginLeft="3px";k.appendChild(a);
+m=a.getContext("2d");m.fillStyle="rgb("+b.fps.bg.r+","+b.fps.bg.g+","+b.fps.bg.b+")";m.fillRect(0,0,a.width,a.height);y=m.getImageData(0,0,a.width,a.height);f=document.createElement("div");f.style.backgroundColor="rgb("+Math.floor(b.ms.bg.r/2)+","+Math.floor(b.ms.bg.g/2)+","+Math.floor(b.ms.bg.b/2)+")";f.style.padding="2px 0px 3px 0px";f.style.display="none";g.appendChild(f);c=document.createElement("div");c.style.fontFamily="Helvetica, Arial, sans-serif";c.style.textAlign="left";c.style.fontSize=
+"9px";c.style.color="rgb("+b.ms.fg.r+","+b.ms.fg.g+","+b.ms.fg.b+")";c.style.margin="0px 0px 1px 3px";c.innerHTML='<span style="font-weight:bold">MS</span>';f.appendChild(c);a=document.createElement("canvas");a.width=74;a.height=30;a.style.display="block";a.style.marginLeft="3px";f.appendChild(a);o=a.getContext("2d");o.fillStyle="rgb("+b.ms.bg.r+","+b.ms.bg.g+","+b.ms.bg.b+")";o.fillRect(0,0,a.width,a.height);B=o.getImageData(0,0,a.width,a.height);try{performance&&performance.memory&&performance.memory.totalJSHeapSize&&
+(t=3)}catch(G){}h=document.createElement("div");h.style.backgroundColor="rgb("+Math.floor(b.mb.bg.r/2)+","+Math.floor(b.mb.bg.g/2)+","+Math.floor(b.mb.bg.b/2)+")";h.style.padding="2px 0px 3px 0px";h.style.display="none";g.appendChild(h);i=document.createElement("div");i.style.fontFamily="Helvetica, Arial, sans-serif";i.style.textAlign="left";i.style.fontSize="9px";i.style.color="rgb("+b.mb.fg.r+","+b.mb.fg.g+","+b.mb.fg.b+")";i.style.margin="0px 0px 1px 3px";i.innerHTML='<span style="font-weight:bold">MB</span>';
+h.appendChild(i);a=document.createElement("canvas");a.width=74;a.height=30;a.style.display="block";a.style.marginLeft="3px";h.appendChild(a);q=a.getContext("2d");q.fillStyle="#301010";q.fillRect(0,0,a.width,a.height);E=q.getImageData(0,0,a.width,a.height);return{domElement:g,update:function(){u++;j=(new Date).getTime();n=j-F;z=Math.min(z,n);A=Math.max(A,n);s(B.data,Math.min(30,30-n/200*30),"ms");c.innerHTML='<span style="font-weight:bold">'+n+" MS</span> ("+z+"-"+A+")";o.putImageData(B,0,0);F=j;if(j>
+v+1E3){l=Math.round(u*1E3/(j-v));w=Math.min(w,l);x=Math.max(x,l);s(y.data,Math.min(30,30-l/100*30),"fps");d.innerHTML='<span style="font-weight:bold">'+l+" FPS</span> ("+w+"-"+x+")";m.putImageData(y,0,0);if(t==3)p=performance.memory.usedJSHeapSize*9.54E-7,C=Math.min(C,p),D=Math.max(D,p),s(E.data,Math.min(30,30-p/2),"mb"),i.innerHTML='<span style="font-weight:bold">'+Math.round(p)+" MB</span> ("+Math.round(C)+"-"+Math.round(D)+")",q.putImageData(E,0,0);v=j;u=0}}}};
+
+Constants={},Constants.PI=Math.PI,Constants.C_PR=Math.PI/180,Constants.VLEV=2,Constants.EPS=1e-7,Constants.c=.105,Constants.LN10=Math.log(10),Constants.PIOVER2=Math.PI/2,Constants.TWOPI=2*Math.PI,Constants.TWOTHIRD=2/3,Constants.ARCSECOND_RADIAN=484813681109536e-20,SpatialVector=function(){function t(t,s,i){"use strict";this.x=t,this.y=s,this.z=i,this.ra_=0,this.dec_=0,this.okRaDec_=!1}return t.prototype.setXYZ=function(t,s,i){this.x=t,this.y=s,this.z=i,this.okRaDec_=!1},t.prototype.length=function(){"use strict";return Math.sqrt(this.lengthSquared())},t.prototype.lengthSquared=function(){"use strict";return this.x*this.x+this.y*this.y+this.z*this.z},t.prototype.normalized=function(){"use strict";var t=this.length();this.x/=t,this.y/=t,this.z/=t},t.prototype.set=function(t,s){"use strict";this.ra_=t,this.dec_=s,this.okRaDec_=!0,this.updateXYZ()},t.prototype.angle=function(t){"use strict";var s=this.y*t.z-this.z*t.y,i=this.z*t.x-this.x*t.z,n=this.x*t.y-this.y*t.x,a=Math.sqrt(s*s+i*i+n*n);return Math.abs(Math.atan2(a,dot(t)))},t.prototype.get=function(){"use strict";return[x,y,z]},t.prototype.toString=function(){"use strict";return"SpatialVector["+this.x+", "+this.y+", "+this.z+"]"},t.prototype.cross=function(s){"use strict";return new t(this.y*s.z-s.y*this.z,this.z*s.x-s.z*this.x,this.x*s.y-s.x()*this.y)},t.prototype.equal=function(t){"use strict";return this.x==t.x&&this.y==t.y&&this.z==t.z()?!0:!1},t.prototype.mult=function(s){"use strict";return new t(s*this.x,s*this.y,s*this.z)},t.prototype.dot=function(t){"use strict";return this.x*t.x+this.y*t.y+this.z*t.z},t.prototype.add=function(s){"use strict";return new t(this.x+s.x,this.y+s.y,this.z+s.z)},t.prototype.sub=function(s){"use strict";return new t(this.x-s.x,this.y-s.y,this.z-s.z)},t.prototype.dec=function(){"use strict";return this.okRaDec_||(this.normalized(),this.updateRaDec()),this.dec_},t.prototype.ra=function(){"use strict";return this.okRaDec_||(this.normalized(),this.updateRaDec()),this.ra_},t.prototype.updateXYZ=function(){"use strict";var t=Math.cos(this.dec_*Constants.C_PR);this.x=Math.cos(this.ra_*Constants.C_PR)*t,this.y=Math.sin(this.ra_*Constants.C_PR)*t,this.z=Math.sin(this.dec_*Constants.C_PR)},t.prototype.updateRaDec=function(){"use strict";this.dec_=Math.asin(this.z)/Constants.C_PR;var t=Math.cos(this.dec_*Constants.C_PR);this.ra_=t>Constants.EPS||-Constants.EPS>t?this.y>Constants.EPS||this.y<-Constants.EPS?0>this.y?360-Math.acos(this.x/t)/Constants.C_PR:Math.acos(this.x/t)/Constants.C_PR:0>this.x?180:0:0,this.okRaDec_=!0},t.prototype.toRaRadians=function(){"use strict";var t=0;return(0!=this.x||0!=this.y)&&(t=Math.atan2(this.y,this.x)),0>t&&(t+=2*Math.PI),t},t.prototype.toDeRadians=function(){var t=z/this.length(),s=Math.acos(t);return Math.PI/2-s},t}(),AngularPosition=function(){return AngularPosition=function(t,s){"use strict";this.theta=t,this.phi=s},AngularPosition.prototype.toString=function(){"use strict";return"theta: "+this.theta+", phi: "+this.phi},AngularPosition}(),LongRangeSetBuilder=function(){function t(){this.items=[]}return t.prototype.appendRange=function(t,s){for(var i=t;s>=i;i++)i in this.items||this.items.push(i)},t}(),HealpixIndex=function(){function t(t){"use strict";this.nside=t}return t.NS_MAX=8192,t.ORDER_MAX=13,t.NSIDELIST=[1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192],t.JRLL=[2,2,2,2,3,3,3,3,4,4,4,4],t.JPLL=[1,3,5,7,0,2,4,6,1,3,5,7],t.XOFFSET=[-1,-1,0,1,1,1,0,-1],t.YOFFSET=[0,1,1,1,0,-1,-1,-1],t.FACEARRAY=[[8,9,10,11,-1,-1,-1,-1,10,11,8,9],[5,6,7,4,8,9,10,11,9,10,11,8],[-1,-1,-1,-1,5,6,7,4,-1,-1,-1,-1],[4,5,6,7,11,8,9,10,11,8,9,10],[0,1,2,3,4,5,6,7,8,9,10,11],[1,2,3,0,0,1,2,3,5,6,7,4],[-1,-1,-1,-1,7,4,5,6,-1,-1,-1,-1],[3,0,1,2,3,0,1,2,4,5,6,7],[2,3,0,1,-1,-1,-1,-1,0,1,2,3]],t.SWAPARRAY=[[0,0,0,0,0,0,0,0,3,3,3,3],[0,0,0,0,0,0,0,0,6,6,6,6],[0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,5,5,5,5],[0,0,0,0,0,0,0,0,0,0,0,0],[5,5,5,5,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0],[6,6,6,6,0,0,0,0,0,0,0,0],[3,3,3,3,0,0,0,0,0,0,0,0]],t.Z0=Constants.TWOTHIRD,t.prototype.init=function(){"use strict";var s=256;this.ctab=Array(s),this.utab=Array(s);for(var i=0;256>i;++i)this.ctab[i]=1&i|(2&i)<<7|(4&i)>>1|(8&i)<<6|(16&i)>>2|(32&i)<<5|(64&i)>>3|(128&i)<<4,this.utab[i]=1&i|(2&i)<<1|(4&i)<<2|(8&i)<<3|(16&i)<<4|(32&i)<<5|(64&i)<<6|(128&i)<<7;this.nl2=2*this.nside,this.nl3=3*this.nside,this.nl4=4*this.nside,this.npface=this.nside*this.nside,this.ncap=2*this.nside*(this.nside-1),this.npix=12*this.npface,this.fact2=4/this.npix,this.fact1=(this.nside<<1)*this.fact2,this.order=t.nside2order(this.nside)},t.calculateNSide=function(s){for(var i=0,n=s*s,a=180/Constants.PI,e=3600*3600*4*Constants.PI*a*a,h=Utils.castToInt(e/n),r=h/12,o=Math.sqrt(r),c=t.NS_MAX,u=0,p=0;t.NSIDELIST.length>p;p++)if(c>=Math.abs(o-t.NSIDELIST[p])&&(c=Math.abs(o-t.NSIDELIST[p]),i=t.NSIDELIST[p],u=p),o>i&&t.NS_MAX>o&&(i=t.NSIDELIST[u+1]),o>t.NS_MAX)return console.log("nside cannot be bigger than "+t.NS_MAX),t.NS_MAX;return i},t.nside2order=function(s){"use strict";return(s&s-1)>0?-1:Utils.castToInt(t.log2(s))},t.log2=function(t){"use strict";return Math.log(t)/Math.log(2)},t.prototype.ang2pix_nest=function(s,i){"use strict";var n,a,e,h,r,o,c,u,p,l,d,f,I;if(i>=Constants.TWOPI&&(i-=Constants.TWOPI),0>i&&(i+=Constants.TWOPI),s>Constants.PI||0>s)throw{name:"Illegal argument",message:"theta must be between 0 and "+Constants.PI};if(i>Constants.TWOPI||0>i)throw{name:"Illegal argument",message:"phi must be between 0 and "+Constants.TWOPI};if(a=Math.cos(s),e=Math.abs(a),h=i/Constants.PIOVER2,t.Z0>=e){var M=this.nside*(.5+h),y=this.nside*.75*a,u=M-y,p=M+y;o=u>>this.order,c=p>>this.order,d=o==c?4==o?4:o+4:c>o?o:c+8,f=Utils.castToInt(p&this.nside-1),I=Utils.castToInt(this.nside-(u&this.nside-1)-1)}else{l=Utils.castToInt(h),l>=4&&(l=3),r=h-l;var g=this.nside*Math.sqrt(3*(1-e));u=Utils.castToInt(r*g),p=Utils.castToInt((1-r)*g),u=Math.min(t.NS_MAX-1,u),p=Math.min(t.NS_MAX-1,p),a>=0?(d=l,f=Utils.castToInt(this.nside-p-1),I=Utils.castToInt(this.nside-u-1)):(d=l+8,f=u,I=p)}return n=this.xyf2nest(f,I,d)},t.prototype.xyf2nest=function(t,s,i){"use strict";return(i<<2*this.order)+(this.utab[255&t]|this.utab[255&t>>8]<<16|this.utab[255&t>>16]<<32|this.utab[255&t>>24]<<48|this.utab[255&s]<<1|this.utab[255&s>>8]<<17|this.utab[255&s>>16]<<33|this.utab[255&s>>24]<<49)},t.prototype.nest2xyf=function(t){"use strict";var s={};s.face_num=t>>2*this.order;var i=t&this.npface-1,n=(93823560581120&i)>>16|(614882086624428e4&i)>>31|21845&i|(1431633920&i)>>15;return s.ix=this.ctab[255&n]|this.ctab[255&n>>8]<<4|this.ctab[255&n>>16]<<16|this.ctab[255&n>>24]<<20,i>>=1,n=(93823560581120&i)>>16|(614882086624428e4&i)>>31|21845&i|(1431633920&i)>>15,s.iy=this.ctab[255&n]|this.ctab[255&n>>8]<<4|this.ctab[255&n>>16]<<16|this.ctab[255&n>>24]<<20,s},t.prototype.pix2ang_nest=function(s){"use strict";if(0>s||s>this.npix-1)throw{name:"Illegal argument",message:"ipix out of range"};var i,n,a,e=this.nest2xyf(s),h=e.ix,r=e.iy,o=e.face_num,c=(t.JRLL[o]<<this.order)-h-r-1;this.nside>c?(i=c,n=1-i*i*this.fact2,a=0):c>this.nl3?(i=this.nl4-c,n=i*i*this.fact2-1,a=0):(i=this.nside,n=(this.nl2-c)*this.fact1,a=1&c-this.nside);var u=Math.acos(n),p=(t.JPLL[o]*i+h-r+1+a)/2;p>this.nl4&&(p-=this.nl4),1>p&&(p+=this.nl4);var l=(p-.5*(a+1))*(Constants.PIOVER2/i);return{theta:u,phi:l}},t.nside2Npix=function(s){"use strict";if(0>s||(s&-s)!=s||s>t.NS_MAX)throw{name:"Illegal argument",message:"nside should be >0, power of 2, <"+t.NS_MAX};var i=12*s*s;return i},t.prototype.xyf2ring=function(s,i,n){"use strict";var a,e,h,r=t.JRLL[n]*this.nside-s-i-1;this.nside>r?(a=r,h=2*a*(a-1),e=0):r>3*this.nside?(a=this.nl4-r,h=this.npix-2*(a+1)*a,e=0):(a=this.nside,h=this.ncap+(r-this.nside)*this.nl4,e=1&r-this.nside);var o=(t.JPLL[n]*a+s-i+1+e)/2;return o>this.nl4?o-=this.nl4:1>o&&(o+=this.nl4),h+o-1},t.prototype.nest2ring=function(t){"use strict";var s=this.nest2xyf(t),i=this.xyf2ring(s.ix,s.iy,s.face_num);return i},t.prototype.corners_nest=function(t,s){"use strict";var i=this.nest2ring(t);return this.corners_ring(i,s)},t.prototype.pix2ang_ring=function(t){"use strict";var s,i,n,a,e,h,r,o,c;if(0>t||t>this.npix-1)throw{name:"Illegal argument",message:"ipix out of range"};return h=t+1,this.ncap>=h?(o=h/2,c=Utils.castToInt(o),n=Utils.castToInt(Math.sqrt(o-Math.sqrt(c)))+1,a=h-2*n*(n-1),s=Math.acos(1-n*n*this.fact2),i=(a-.5)*Constants.PI/(2*n)):this.npix-this.ncap>t?(e=t-this.ncap,n=e/this.nl4+this.nside,a=e%this.nl4+1,r=(1&n+this.nside)>0?1:.5,s=Math.acos((this.nl2-n)*this.fact1),i=(a-r)*Constants.PI/this.nl2):(e=this.npix-t,n=Utils.castToInt(.5*(1+Math.sqrt(2*e-1))),a=4*n+1-(e-2*n*(n-1)),s=Math.acos(-1+Math.pow(n,2)*this.fact2),i=(a-.5)*Constants.PI/(2*n)),[s,i]},t.prototype.ring=function(t){"use strict";var s,i,n=0,a=t+1,e=0;return this.ncap>=a?(i=a/2,e=Utils.castToInt(i),n=Utils.castToInt(Math.sqrt(i-Math.sqrt(e)))+1):this.nl2*(5*this.nside+1)>=a?(s=Utils.castToInt(a-this.ncap-1),n=Utils.castToInt(s/this.nl4+this.nside)):(s=this.npix-a+1,i=s/2,e=Utils.castToInt(i),n=Utils.castToInt(Math.sqrt(i-Math.sqrt(e)))+1,n=this.nl4-n),n},t.prototype.integration_limits_in_costh=function(t){"use strict";var s,i,n,a;return a=1*this.nside,this.nside>=t?(i=1-Math.pow(t,2)/3/this.npface,n=1-Math.pow(t-1,2)/3/this.npface,s=t==this.nside?2*(this.nside-1)/3/a:1-Math.pow(t+1,2)/3/this.npface):this.nl3>t?(i=2*(2*this.nside-t)/3/a,n=2*(2*this.nside-t+1)/3/a,s=2*(2*this.nside-t-1)/3/a):(n=t==this.nl3?2*(-this.nside+1)/3/a:-1+Math.pow(4*this.nside-t+1,2)/3/this.npface,s=-1+Math.pow(this.nl4-t-1,2)/3/this.npface,i=-1+Math.pow(this.nl4-t,2)/3/this.npface),[n,i,s]},t.prototype.pixel_boundaries=function(t,s,i,n){var a,e,h,r,o,c,u,p,l=1*this.nside;if(Math.abs(n)>=1-1/3/this.npface)return u=i*Constants.PIOVER2,p=(i+1)*Constants.PIOVER2,[u,p];if(1.5*n>=1)a=Math.sqrt(3*(1-n)),e=1/l/a,h=s,r=h-1,o=t-s,c=o+1,u=Constants.PIOVER2*(Math.max(r*e,1-c*e)+i),p=Constants.PIOVER2*(Math.min(1-o*e,h*e)+i);else if(1.5*n>-1){var d=.5*(1-1.5*n),f=d+1,I=this.nside+t%2;h=s-(I-t)/2,r=h-1,o=(I+t)/2-s,c=o+1,u=Constants.PIOVER2*(Math.max(f-c/l,-d+r/l)+i),p=Constants.PIOVER2*(Math.min(f-o/l,-d+h/l)+i)}else{a=Math.sqrt(3*(1+n)),e=1/l/a;var M=2*this.nside;h=t-M+s,r=h-1,o=M-s,c=o+1,u=Constants.PIOVER2*(Math.max(1-(M-r)*e,(M-c)*e)+i),p=Constants.PIOVER2*(Math.min(1-(M-h)*e,(M-o)*e)+i)}return[u,p]},t.vector=function(t,s){"use strict";var i=1*Math.sin(t)*Math.cos(s),n=1*Math.sin(t)*Math.sin(s),a=1*Math.cos(t);return new SpatialVector(i,n,a)},t.prototype.corners_ring=function(s,i){"use strict";var n=2*i+2,a=Array(n),e=this.pix2ang_ring(s),h=Math.cos(e[0]),r=e[0],o=e[1],c=Utils.castToInt(o/Constants.PIOVER2),u=this.ring(s),p=Math.min(u,Math.min(this.nside,this.nl4-u)),l=0,d=Constants.PIOVER2/p;l=u>=this.nside&&this.nl3>=u?Utils.castToInt(o/d+u%2/2)+1:Utils.castToInt(o/d)+1,l-=c*p;var f=n/2,I=this.integration_limits_in_costh(u),M=Math.acos(I[0]),y=Math.acos(I[2]),g=this.pixel_boundaries(u,l,c,I[0]);if(a[0]=l>p/2?t.vector(M,g[1]):t.vector(M,g[0]),g=this.pixel_boundaries(u,l,c,I[2]),a[f]=l>p/2?t.vector(y,g[1]):t.vector(y,g[0]),1==i){var P=Math.acos(I[1]);g=this.pixel_boundaries(u,l,c,I[1]),a[1]=t.vector(P,g[0]),a[3]=t.vector(P,g[1])}else for(var x=I[2]-I[0],C=x/(i+1),v=1;i>=v;v++)h=I[0]+C*v,r=Math.acos(h),g=this.pixel_boundaries(u,l,c,h),a[v]=t.vector(r,g[0]),a[n-v]=t.vector(r,g[1]);return a},t.vec2Ang=function(t){"use strict";var s=t.z/t.length(),i=Math.acos(s),n=0;return(0!=t.x||0!=t.y)&&(n=Math.atan2(t.y,t.x)),0>n&&(n+=2*Math.PI),[i,n]},t.prototype.queryDisc=function(s,i,n,a){"use strict";if(0>i||i>Constants.PI)throw{name:"Illegal argument",message:"angular radius is in RADIAN and should be in [0,pi]"};var e,h,r,o,c,u,p,l,d,f,I,M,y,g,P,x,C,v,_,R=new LongRangeSetBuilder,T=null,c=i;if(a&&(c+=Constants.PI/this.nl4),T=t.vec2Ang(s),u=T[0],p=T[1],I=this.fact2,M=this.fact1,o=Math.cos(u),_=1/Math.sqrt((1-o)*(1+o)),g=u-c,P=u+c,l=Math.cos(c),C=Math.cos(g),e=this.ringAbove(C)+1,x=Math.cos(P),h=this.ringAbove(x),e>h&&0==h&&(h=e),0>=g)for(var m=1;e>m;++m)this.inRing(m,0,Math.PI,R);for(r=e;h>=r;++r)v=this.nside>r?1-r*r*I:this.nl3>=r?(this.nl2-r)*M:-1+(this.nl4-r)*(this.nl4-r)*I,d=(l-v*o)*_,f=1-v*v-d*d,y=Math.atan2(Math.sqrt(f),d),isNaN(y)&&(y=c),this.inRing(r,p,y,R);if(P>=Math.PI)for(var m=h+1;this.nl4>m;++m)this.inRing(m,0,Math.PI,R,!1);var b;if(n){for(var S=R.items,U=[],A=0;S.length>A;A++){var O=this.ring2nest(S[A]);U.indexOf(O)>=0||U.push(O)}b=U}else b=R.items;return b},t.prototype.inRing=function(t,s,i,n,a){"use strict";var e,h,r,o,c=!1,u=!1,p=1e-12,l=0,d=0,f=0,I=0,M=(s-i)%Constants.TWOPI-p,y=s+i+p,g=(s+i)%Constants.TWOPI+p;if(p>Math.abs(i-Constants.PI)&&(c=!0),t>=this.nside&&this.nl3>=t?(d=t-this.nside+1,r=this.ncap+this.nl4*(d-1),o=r+this.nl4-1,e=d%2,h=this.nl4):(this.nside>t?(d=t,r=2*d*(d-1),o=r+4*d-1):(d=4*this.nside-t,r=this.npix-2*d*(d+1),o=r+4*d-1),h=4*d,e=1),c)return n.appendRange(r,o),void 0;if(l=e/2,a)f=Math.round(h*M/Constants.TWOPI-l),I=Math.round(h*y/Constants.TWOPI-l),f%=h,I>h&&(I%=h);else{if(f=Math.ceil(h*M/Constants.TWOPI-l),I=Utils.castToInt(h*g/Constants.TWOPI-l),f>I&&1==t&&(I=Utils.castToInt(h*y/Constants.TWOPI-l)),f==I+1&&(f=I),1==f-I&&Constants.PI>i*h)return console.log("the interval is too small and avay from center"),void 0;f=Math.min(f,h-1),I=Math.max(I,0)}if(f>I&&(u=!0),u)f+=r,I+=r,n.appendRange(r,I),n.appendRange(f,o);else{if(0>f)return f=Math.abs(f),n.appendRange(r,r+I),n.appendRange(o-f+1,o),void 0;f+=r,I+=r,n.appendRange(f,I)}},t.prototype.ringAbove=function(t){"use strict";var s=Math.abs(t);if(s>Constants.TWOTHIRD){var i=Utils.castToInt(this.nside*Math.sqrt(3*(1-s)));return t>0?i:4*this.nside-i-1}return Utils.castToInt(this.nside*(2-1.5*t))},t.prototype.ring2nest=function(t){"use strict";var s=this.ring2xyf(t);return this.xyf2nest(s.ix,s.iy,s.face_num)},t.prototype.ring2xyf=function(s){"use strict";var i,n,a,e,h={};if(this.ncap>s){i=Utils.castToInt(.5*(1+Math.sqrt(1+2*s))),n=s+1-2*i*(i-1),a=0,e=i,h.face_num=0;var r=n-1;r>=2*i&&(h.face_num=2,r-=2*i),r>=i&&++h.face_num}else if(this.npix-this.ncap>s){var o=s-this.ncap;this.order>=0?(i=(o>>this.order+2)+this.nside,n=(o&this.nl4-1)+1):(i=o/this.nl4+this.nside,n=o%this.nl4+1),a=1&i+this.nside,e=this.nside;var c,u,p=i-this.nside+1,l=this.nl2+2-p;this.order>=0?(c=n-Utils.castToInt(p/2)+this.nside-1>>this.order,u=n-Utils.castToInt(l/2)+this.nside-1>>this.order):(c=(n-Utils.castToInt(p/2)+this.nside-1)/this.nside,u=(n-Utils.castToInt(l/2)+this.nside-1)/this.nside),h.face_num=u==c?4==u?4:Utils.castToInt(u)+4:c>u?Utils.castToInt(u):Utils.castToInt(c)+8}else{var o=this.npix-s;i=Utils.castToInt(.5*(1+Math.sqrt(2*o-1))),n=4*i+1-(o-2*i*(i-1)),a=0,e=i,i=2*this.nl2-i,h.face_num=8;var r=n-1;r>=2*e&&(h.face_num=10,r-=2*e),r>=e&&++h.face_num}var d=i-t.JRLL[h.face_num]*this.nside+1,f=2*n-t.JPLL[h.face_num]*e-a-1;return f>=this.nl2&&(f-=8*this.nside),h.ix=f-d>>1,h.iy=-(f+d)>>1,h},t}(),Utils=function(){},Utils.radecToPolar=function(t,s){return{theta:Math.PI/2-s/180*Math.PI,phi:t/180*Math.PI}},Utils.polarToRadec=function(t,s){return{ra:180*s/Math.PI,dec:180*(Math.PI/2-t)/Math.PI}},Utils.castToInt=function(t){return t>0?Math.floor(t):Math.ceil(t)};//=================================
+//            AstroMath
+//=================================
+
+// Class AstroMath having 'static' methods
+function AstroMath() {}
+
+// Constant for conversion Degrees => Radians (rad = deg*AstroMath.D2R)
+AstroMath.D2R = Math.PI/180.0;
+// Constant for conversion Radians => Degrees (deg = rad*AstroMath.R2D)
+AstroMath.R2D = 180.0/Math.PI;
+/**
+ * Function sign
+ * @param x value for checking the sign
+ * @return -1, 0, +1 respectively if x < 0, = 0, > 0
+ */
+AstroMath.sign = function(x) { return x > 0 ? 1 : (x < 0 ? -1 : 0 ); };
+
+/**
+ * Function cosd(degrees)
+ * @param x angle in degrees
+ * @returns the cosine of the angle
+ */
+AstroMath.cosd = function(x) {
+	if (x % 90 == 0) {
+		var i = Math.abs(Math.floor(x / 90 + 0.5)) % 4;
+		switch (i) {
+			case 0:	return 1;
+			case 1:	return 0;
+			case 2:	return -1;
+			case 3:	return 0;
+		}
+	}
+	return Math.cos(x*AstroMath.D2R);
+};
+
+/**
+ * Function sind(degrees)
+ * @param x angle in degrees
+ * @returns the sine of the angle
+ */
+AstroMath.sind = function(x) {
+	if (x % 90 === 0) {
+		var i = Math.abs(Math.floor(x / 90 - 0.5)) % 4;
+		switch (i) {
+			case 0:	return 1;
+			case 1:	return 0;
+			case 2:	return -1;
+			case 3:	return 0;
+		}
+	}
+
+	return Math.sin(x*AstroMath.D2R);
+};
+
+/**
+ * Function tand(degrees)
+ * @param x angle in degrees
+ * @returns the tangent of the angle
+ */
+AstroMath.tand = function(x) {
+	var resid;
+
+	resid = x % 360;
+	if (resid == 0 || Math.abs(resid) == 180) {
+		return 0;
+	} else if (resid == 45 || resid == 225) {
+		return 1;
+	} else if (resid == -135 || resid == -315) {
+		return -1
+	}
+
+	return Math.tan(x * AstroMath.D2R);
+};
+
+/**
+ * Function asin(degrees)
+ * @param sine value [0,1]
+ * @return the angle in degrees
+ */
+AstroMath.asind = function(x) { return Math.asin(x)*AstroMath.R2D; };
+
+/**
+ * Function acos(degrees)
+ * @param cosine value [0,1]
+ * @return the angle in degrees
+ */
+AstroMath.acosd = function(x) { return Math.acos(x)*AstroMath.R2D; };
+
+/**
+ * Function atan(degrees)
+ * @param tangent value
+ * @return the angle in degrees
+ */
+AstroMath.atand = function(x) { return Math.atan(x)*AstroMath.R2D; };
+
+/**
+ * Function atan2(y,x)
+ * @param y y component of the vector
+ * @param x x component of the vector
+ * @return the angle in radians
+ */
+AstroMath.atan2 = function(y,x) {
+	if (y != 0.0) {
+		var sgny = AstroMath.sign(y);
+		if (x != 0.0) {
+			var phi = Math.atan(Math.abs(y/x));
+			if (x > 0.0) return phi*sgny;
+			else if (x < 0) return (Math.PI-phi)*sgny;
+		} else return (Math.PI/2)*sgny;
+	} else {
+		return x > 0.0 ? 0.0 : (x < 0 ? Math.PI : 0.0/0.0);
+	}
+}
+
+/**
+ * Function atan2d(y,x)
+ * @param y y component of the vector
+ * @param x x component of the vector
+ * @return the angle in degrees
+ */
+AstroMath.atan2d = function(y,x) {
+	return AstroMath.atan2(y,x)*AstroMath.R2D;
+}
+
+/*=========================================================================*/
+/**
+ * Computation of hyperbolic cosine
+ * @param x argument
+ */
+AstroMath.cosh = function(x) {
+	return (Math.exp(x)+Math.exp(-x))/2;
+}
+
+/**
+ * Computation of hyperbolic sine
+ * @param x argument
+ */
+AstroMath.sinh = function(x) {
+	return (Math.exp(x)-Math.exp(-x))/2;
+}
+
+/**
+ * Computation of hyperbolic tangent
+ * @param x argument
+ */
+AstroMath.tanh = function(x) {
+	return (Math.exp(x)-Math.exp(-x))/(Math.exp(x)+Math.exp(-x));
+}
+
+/**
+ * Computation of Arg cosh
+ * @param x argument in degrees. Must be in the range [ 1, +infinity ]
+ */
+AstroMath.acosh = function(x) {
+	return(Math.log(x+Math.sqrt(x*x-1.0)));
+}
+
+/**
+ * Computation of Arg sinh
+ * @param x argument in degrees
+ */
+AstroMath.asinh = function(x) {
+	return(Math.log(x+Math.sqrt(x*x+1.0)));
+}
+
+/**
+ * Computation of Arg tanh
+ * @param x argument in degrees. Must be in the range ] -1, +1 [
+ */
+AstroMath.atanh = function(x) {
+	return(0.5*Math.log((1.0+x)/(1.0-x)));
+}
+
+//=============================================================================
+//      Special Functions using trigonometry
+//=============================================================================
+/**
+ * Computation of sin(x)/x
+ *	@param x in degrees.
+ * For small arguments x <= 0.001, use approximation
+ */
+AstroMath.sinc = function(x) {
+	var ax = Math.abs(x);
+	var y;
+
+	if (ax <= 0.001) {
+		ax *= ax;
+		y = 1 - ax*(1.0-ax/20.0)/6.0;
+	} else {
+		y = Math.sin(ax)/ax;
+	}
+
+	return y;
+}
+
+/**
+ * Computes asin(x)/x
+ * @param x in degrees.
+ * For small arguments x <= 0.001, use an approximation
+ */
+AstroMath.asinc = function(x) {
+	var ax = Math.abs(x);
+	var y;
+
+	if (ax <= 0.001) {
+		ax *= ax;
+		y = 1 + ax*(6.0 + ax*(9.0/20.0))/6.0;
+	} else {
+		y = Math.asin(ax)/ax;	// ???? radians ???
+	}
+
+	return (y);
+}
+
+
+//=============================================================================
+/**
+ * Computes the hypotenuse of x and y
+ * @param x value
+ * @param y value
+ * @return sqrt(x*x+y*y)
+ */
+AstroMath.hypot = function(x,y) {
+	return Math.sqrt(x*x+y*y);
+}
+
+/** Generate the rotation matrix from the Euler angles
+ * @param z	Euler angle
+ * @param theta	Euler angle
+ * @param zeta	Euler angles
+ * @return R [3][3]		the rotation matrix
+ * The rotation matrix is defined by:<pre>
+ *    R =      R_z(-z)      *        R_y(theta)     *     R_z(-zeta)
+ *   |cos.z -sin.z  0|   |cos.the  0 -sin.the|   |cos.zet -sin.zet 0|
+ * = |sin.z  cos.z  0| x |   0     1     0   | x |sin.zet  cos.zet 0|
+ *   |   0      0   1|   |sin.the  0  cos.the|   |   0        0    1|
+ * </pre>
+ */
+AstroMath.eulerMatrix = function(z, theta, zeta) {
+	var R = new Array(3);
+	R[0] = new Array(3);
+	R[1] = new Array(3);
+	R[2] = new Array(3);
+	var cosdZ = AstroMath.cosd(z);
+	var sindZ = AstroMath.sind(z);
+	var cosdTheta = AstroMath.cosd(theta);
+	var w = AstroMath.sind(theta) ;
+	var cosdZeta = AstroMath.cosd(zeta);
+	var sindZeta = AstroMath.sind(zeta);
+
+	R[0][0] = cosdZeta*cosdTheta*cosdZ - sindZeta*sindZ;
+	R[0][1] = -sindZeta*cosdTheta*cosdZ - cosdZeta*sindZ;
+	R[0][2] = -w*cosdZ;
+
+	R[1][0] = cosdZeta*cosdTheta*sindZ + sindZeta*cosdZ;
+	R[1][1] = -sindZeta*cosdTheta*sindZ + cosdZeta*cosdZ;
+	R[1][2] = -w*sindZ;
+
+	R[2][0] = -w*cosdZeta;
+	R[2][1] = -w*cosdZ;
+	R[2][2] = cosdTheta;
+	return R ;
+};
+
+
+AstroMath.displayMatrix = function(m) {
+	// Number of rows
+	var nbrows = m.length;
+	// Max column count
+	var nbcols = 0
+	for (var i=0; i<nbrows; i++) {
+		if (m[i].length > nbcols) nbcols = m[i].length;
+	}
+	var str = '<table>\n';
+	for (var i=0; i<nbrows; i++) {
+		str += '<tr>';
+		for (var j=0; j<nbrows; j++) {
+			str += '<td>';
+			if (i < m[i].length)
+				str += (m[i][j]).toString();
+			str += '</td>';
+		}
+		str += '</td>\n';
+	}
+	str += '</table>\n';
+
+	return str;
+}
+function Projection(lon0, lat0) {
+	this.PROJECTION = Projection.PROJ_TAN;
+	this.ROT = this.tr_oR(lon0, lat0);
+
+    this.longitudeIsReversed = false;
+}
+
+//var ROT;
+//var PROJECTION = Projection.PROJ_TAN;	// Default projection
+
+
+Projection.PROJ_TAN = 1;	/* Gnomonic projection*/
+Projection.PROJ_TAN2 = 2;	/* Stereographic projection*/
+Projection.PROJ_STG = 2;
+Projection.PROJ_SIN = 3;	/* Orthographic		*/
+Projection.PROJ_SIN2 = 4;	/* Equal-area 		*/
+Projection.PROJ_ZEA = 4;	/* Zenithal Equal-area 	*/
+Projection.PROJ_ARC = 5;	/* For Schmidt plates	*/
+Projection.PROJ_SCHMIDT = 5;	/* For Schmidt plates	*/
+Projection.PROJ_AITOFF = 6;	/* Aitoff Projection	*/
+Projection.PROJ_AIT = 6;	/* Aitoff Projection	*/
+Projection.PROJ_GLS = 7;	/* Global Sin (Sanson)	*/
+Projection.PROJ_MERCATOR = 8;
+Projection.PROJ_MER = 8;
+Projection.PROJ_LAM = 9;	/* Lambert Projection	*/
+Projection.PROJ_LAMBERT = 9;
+Projection.PROJ_TSC = 10;	/* Tangent Sph. Cube	*/
+Projection.PROJ_QSC = 11;	/* QuadCube Sph. Cube	*/
+
+Projection.PROJ_LIST = [
+	"Mercator",Projection.PROJ_MERCATOR,
+	"Gnomonic",Projection.PROJ_TAN,
+	"Stereographic",Projection.PROJ_TAN2,
+	"Orthographic",Projection.PROJ_SIN,
+	"Zenithal",Projection.PROJ_ZEA,
+	"Schmidt",Projection.PROJ_SCHMIDT,
+	"Aitoff",Projection.PROJ_AITOFF,
+	"Lambert",Projection.PROJ_LAMBERT,
+//	"Tangential",Projection.PROJ_TSC,
+//	"Quadrilaterized",Projection.PROJ_QSC,
+];
+Projection.PROJ_NAME = [
+	'-', 'Gnomonic', 'Stereographic', 'Orthographic', 'Equal-area', 'Schmidt plates',
+	'Aitoff', 'Global sin', 'Mercator', 'Lambert'
+];
+
+Projection.prototype = {
+
+	/** Set the center of the projection
+	 *
+	 * (ajout T. Boch, 19/02/2013)
+	 *
+	 * */
+	setCenter: function(lon0, lat0) {
+		this.ROT = this.tr_oR(lon0, lat0);
+	},
+
+    /** Reverse the longitude
+      * If set to true, longitudes will increase from left to right
+      *
+      * */
+    reverseLongitude: function(b) {
+        this.longitudeIsReversed = b;
+    },
+
+	/**
+	 * Set the projection to use
+	 * p = projection code
+	 */
+	setProjection: function(p) {
+		this.PROJECTION = p;
+	},
+
+
+	/**
+	 * Computes the projection of 1 point : ra,dec => X,Y
+	 * alpha, delta = longitude, lattitude
+	 */
+	project: function(alpha, delta) {
+        var u1 = this.tr_ou(alpha, delta);	// u1[3]
+		var u2 = this.tr_uu(u1, this.ROT);	// u2[3]
+		var P = this.tr_up(this.PROJECTION, u2);	// P[2] = [X,Y]
+		if (P == null) {
+			return null;
+		}
+
+		if( this.longitudeIsReversed) {
+            return { X: P[0], Y: -P[1] };
+        }
+        else {
+		    return { X: -P[0], Y: -P[1] };
+        }
+        //return { X: -P[0], Y: -P[1] };
+	},
+
+	/**
+	 * Computes the coordinates from a projection point : X,Y => ra,dec
+	 * return o = [ ra, dec ]
+	 */
+	unproject: function(X,Y) {
+		if ( ! this.longitudeIsReversed) {
+            X = -X;
+        }
+		Y = -Y;
+		var u1 = this.tr_pu(this.PROJECTION, X, Y);	// u1[3]
+		var u2 = this.tr_uu1(u1, this.ROT);	// u2[3]
+		var o = this.tr_uo(u2);	// o[2]
+
+/*
+		if (this.longitudeIsReversed) {
+            return { ra: 360-o[0], dec: o[1] };
+        }
+        else {
+		    return { ra: o[0], dec: o[1] };
+        }
+*/
+        return { ra: o[0], dec: o[1] };
+	},
+
+	/**
+	 * Compute projections from unit vector
+	 * The center of the projection correspond to u = [1, 0, 0)
+	 * proj = projection system (integer code like _PROJ_MERCATOR_
+	 * u[3] = unit vector
+	 * return: an array [x,y] or null
+	 */
+	tr_up: function(proj, u) {
+		var x = u[0]; var y = u[1]; var z = u[2];
+		var r, den;
+		var pp;
+		var X,Y;
+
+		r = AstroMath.hypot(x,y);			// r = cos b
+		if (r == 0.0 && z == 0.0) return null;
+
+		switch(proj) {
+			default:
+				pp = null;
+				break;
+
+			case Projection.PROJ_AITOFF:
+				den = Math.sqrt(r*(r+x)/2.0); 		// cos b . cos l/2
+				X = Math.sqrt(2.0*r*(r-x));
+				den = Math.sqrt((1.0 + den)/2.0);
+				X = X / den;
+				Y = z / den;
+				if (y < 0.0) X = -X;
+				pp = [ X, Y];
+				break;
+
+			case Projection.PROJ_GLS:
+				Y = Math.asin(z);				// sin b
+				X = (r != 0) ? Math.atan2(y,x)*r : 0.0;
+				pp = [ X, Y];
+				break;
+
+			case Projection.PROJ_MERCATOR:
+				if (r != 0) {
+					X = Math.atan2(y,x);
+					Y = AstroMath.atanh(z);
+					pp = [ X, Y];
+				} else {
+					pp = null;
+				}
+				break;
+
+			case Projection.PROJ_TAN:
+				if (x > 0.0) {
+					X = y/x;
+					Y = z/x;
+					pp = [ X, Y ];
+				} else {
+					pp = null;
+				}
+				break;
+
+			case Projection.PROJ_TAN2:
+				den = (1.0 + x)/2.0;
+				if (den > 0.0)	{
+					X = y/den;
+					Y = z/den;
+					pp = [ X, Y ];
+				} else {
+					pp = null;
+				}
+			 	break;
+
+			case Projection.PROJ_ARC:
+				if (x <= -1.0) {
+					// Distance of 180 degrees
+					X = Math.PI
+					Y = 0.0;
+				} else {
+					// Arccos(x) = Arcsin(r)
+					r = AstroMath.hypot(y,z);
+					if (x > 0.0) den = AstroMath.asinc(r);
+					else den = Math.acos(x)/r;
+					X = y * den;
+					Y = z * den;
+				}
+				pp = [ X, Y ];
+				break;
+
+			case Projection.PROJ_SIN:
+				if (x >= 0.0) {
+					X = y;
+					Y = z;
+					pp = [ X, Y ];
+				} else {
+					pp = null;
+				}
+				break;
+
+			case Projection.PROJ_SIN2:	// Always possible
+				den = Math.sqrt((1.0 + x)/2.0);
+				if (den != 0)	{
+					X = y / den;
+					Y = z / den;
+				} else {
+					// For x = -1
+					X = 2.0;
+					Y = 0.0;
+				}
+				pp = [ X, Y ];
+				break;
+
+			case Projection.PROJ_LAMBERT:	// Always possible
+				Y = z;
+				X = 0;
+				if (r != 0)	X = Math.atan2(y,x);
+				pp = [ X, Y ];
+				break;
+	  }
+	  return pp;
+	},
+
+	/**
+	 * Computes Unit vector from a position in projection centered at position (0,0).
+	 * proj = projection code
+	 * X,Y : coordinates of the point in the projection
+	 * returns : the unit vector u[3] or a face number for cube projection.
+	 *           null if the point is outside the limits, or if the projection is unknown.
+	 */
+	tr_pu: function( proj, X, Y ) {
+		var r,s,x,y,z;
+
+		switch(proj) {
+			default:
+			return null;
+
+			case Projection.PROJ_AITOFF:
+				// Limit is ellipse with axises
+				// a = 2 * sqrt(2) ,  b = sqrt(2)
+				// Compute dir l/2, b
+				r = X*X/8.e0 + Y*Y/2.e0; 	// 1 - cos b . cos l/2
+				if (r > 1.0) {
+	  				// Test outside domain */
+					return null;
+				}
+				x = 1.0 - r ;	// cos b . cos l/2
+				s = Math.sqrt(1.0 - r/2.0) ;	// sqrt(( 1 + cos b . cos l/2)/2)
+				y = X * s / 2.0;
+				z = Y * s ;
+				// From (l/2,b) to (l,b)
+				r = AstroMath.hypot( x, y ) ;	// cos b
+				if (r != 0.0) {
+					s = x;
+					x = (s*s - y*y) /r;
+					y = 2.0 * s * y/r;
+				}
+				break;
+
+			case Projection.PROJ_GLS:
+				// Limit is |Y| <= pi/2
+				z = Math.sin(Y);
+				r = 1 - z*z;		// cos(b) ** 2
+				if (r < 0.0) {
+					return null;
+				}
+				r = Math.sqrt(r);		// cos b
+				if (r != 0.0) {
+					s = X/r;	// Longitude
+				} else {
+					s = 0.0;	// For poles
+				}
+				x = r * Math.cos(s);
+				y = r * Math.sin(s);
+				break;
+
+			case Projection.PROJ_MERCATOR:
+				z = AstroMath.tanh(Y);
+				r = 1.0/AstroMath.cosh(Y);
+				x = r * Math.cos(X);
+				y = r * Math.sin(X);
+				break;
+
+			case Projection.PROJ_LAMBERT:
+				// Always possible
+				z = Y;
+				r = 1 - z*z;		// cos(b) ** 2
+				if (r < 0.0) {
+					return null;
+				}
+				r = Math.sqrt(r);		// cos b
+				x = r * Math.cos(X);
+				y = r * Math.sin(X);
+				break;
+
+			case Projection.PROJ_TAN:
+				// No limit
+				x = 1.0 / Math.sqrt(1.0 + X*X + Y*Y);
+				y = X * x;
+				z = Y * x;
+				break;
+
+			case Projection.PROJ_TAN2:
+				// No limit
+				r = (X*X + Y*Y)/4.0;
+				s = 1.0 + r;
+				x = (1.0 - r)/s;
+				y = X / s;
+				z = Y / s;
+				break;
+
+			case Projection.PROJ_ARC:
+				// Limit is circle, radius PI
+				r = AstroMath.hypot(X, Y);
+				if (r > Math.PI) {
+					return null;
+				}
+				s = AstroMath.sinc(r);
+				x = Math.cos(r);
+				y = s * X;
+				z = s * Y;
+				break;
+
+			case Projection.PROJ_SIN:
+				// Limit is circle, radius 1
+				s = 1.0 - X*X - Y*Y;
+				if (s < 0.0) {
+					return null;
+				}
+				x = Math.sqrt(s);
+				y = X;
+				z = Y;
+				break;
+
+			case Projection.PROJ_SIN2:
+				// Limit is circle, radius 2	*/
+				r = (X*X + Y*Y)/4.e0;
+				if (r > 1.0) {
+					return null;
+				}
+				s = Math.sqrt(1.0 - r);
+				x = 1.0 - 2.0 * r;
+				y = s * X;
+				z = s * Y;
+				break;
+	  }
+	  return [ x,y,z ];
+	},
+
+	/**
+	 * Creates the rotation matrix R[3][3] defined as
+	 * R[0] (first row) = unit vector towards Zenith
+	 * R[1] (second row) = unit vector towards East
+	 * R[2] (third row) = unit vector towards North
+	 * o[2] original angles
+	 * @return rotation matrix
+	 */
+	tr_oR: function(lon, lat) {
+		var R = new Array(3);
+		R[0] = new Array(3);
+		R[1] = new Array(3);
+		R[2] = new Array(3);
+		R[2][2] =  AstroMath.cosd(lat);
+		R[0][2] =  AstroMath.sind(lat);
+		R[1][1] =  AstroMath.cosd(lon);
+		R[1][0] =  -AstroMath.sind(lon);
+		R[1][2] =  0.0;
+		R[0][0] =  R[2][2] * R[1][1];
+		R[0][1] = -R[2][2] * R[1][0];
+		R[2][0] = -R[0][2] * R[1][1];
+		R[2][1] =  R[0][2] * R[1][0];
+		return R;
+	},
+
+	/**
+	 * Transformation from polar coordinates to Unit vector
+	 * @return U[3]
+	 */
+	tr_ou: function(ra, dec) {
+		var u = new Array(3);
+		var cosdec = AstroMath.cosd(dec);
+
+		u[0] = cosdec * AstroMath.cosd(ra);
+		u[1] = cosdec * AstroMath.sind(ra);
+		u[2] = AstroMath.sind(dec);
+
+		return u;
+	},
+
+	/**
+	 * Rotates the unit vector u1 using the rotation matrix
+	 * u1[3] unit vector
+	 * R[3][3] rotation matrix
+	 * return resulting unit vector u2[3]
+	 */
+	tr_uu: function( u1, R ) {
+		var u2 = new Array(3);
+		var x = u1[0];
+		var y = u1[1];
+		var z = u1[2];
+
+		u2[0] = R[0][0]*x + R[0][1]*y + R[0][2]*z ;
+		u2[1] = R[1][0]*x + R[1][1]*y + R[1][2]*z ;
+		u2[2] = R[2][0]*x + R[2][1]*y + R[2][2]*z ;
+
+		return u2;
+	},
+
+	/**
+	 * reverse rotation the unit vector u1 using the rotation matrix
+	 * u1[3] unit vector
+	 * R[3][3] rotation matrix
+	 * return resulting unit vector u2[3]
+	 */
+	tr_uu1: function( u1 , R) {
+		var u2 = new Array(3);
+		var x = u1[0];
+		var y = u1[1];
+		var z = u1[2];
+
+		u2[0] = R[0][0]*x + R[1][0]*y + R[2][0]*z;
+		u2[1] = R[0][1]*x + R[1][1]*y + R[2][1]*z;
+		u2[2] = R[0][2]*x + R[1][2]*y + R[2][2]*z;
+
+		return u2;
+	},
+
+	/**
+	 * Computes angles from direction cosines
+	 * u[3] = direction cosines vector
+	 * return o = [ ra, dec ]
+	 */
+	tr_uo: function(u) {
+		var x = u[0]; var y = u[1]; var z = u[2];
+		var r2 = x*x + y*y;
+		var ra, dec;
+		if (r2  == 0.0) {
+	 		// in case of poles
+			if (z == 0.0) {
+				return null;
+			}
+			ra = 0.0;
+			dec = z > 0.0 ? 90.0 : -90.0;
+		} else {
+			dec = AstroMath.atand( z / Math.sqrt(r2));
+			ra  = AstroMath.atan2d (y , x );
+			if (ra < 0.0) ra += 360.0;
+		}
+
+		return [ ra, dec ];
+	}
+}
+//=================================
+// Class Coo
+//=================================
+
+/**
+ * Constructor
+ * @param longitude longitude (decimal degrees)
+ * @param latitude latitude (decimal degrees)
+ * @param prec precision
+ * (8: 1/1000th sec, 7: 1/100th sec, 6: 1/10th sec, 5: sec, 4: 1/10th min, 3: min, 2: 1/10th deg, 1: deg
+ */
+function Coo(longitude, latitude, prec) {
+	this.lon = longitude;
+	this.lat = latitude;
+	this.prec = prec;
+	this.frame = null;
+
+	this.computeDirCos();
+}
+
+Coo.factor = [ 3600.0, 60.0, 1.0 ];
+Coo.prototype = {
+	setFrame: function(astroframe) {
+		this.frame = astroframe;
+	},
+	computeDirCos: function() {
+		var coslat = AstroMath.cosd(this.lat);
+
+		this.x = coslat*AstroMath.cosd(this.lon);
+		this.y = coslat*AstroMath.sind(this.lon);
+		this.z = AstroMath.sind(this.lat);
+	},
+	computeLonLat: function() {
+		var r2 = this.x*this.x+this.y*this.y;
+		this.lon = 0.0;
+		if (r2 == 0.0) {
+			// In case of poles
+			if (this.z == 0.0) {
+				this.lon = 0.0/0.0;
+				this.lat = 0.0/0.0;
+			} else {
+				this.lat = (this.z > 0.0) ? 90.0 : -90.0;
+			}
+		} else {
+			this.lon = AstroMath.atan2d(this.y, this.x);
+			this.lat = AstroMath.atan2d(this.z, Math.sqrt(r2));
+			if (this.lon < 0) this.lon += 360.0;
+		}
+	},
+
+  /**
+    * Squared distance between 2 points (= 4.sin<sup>2</sup>(r/2))
+    * @param  pos      another position on the sphere
+    * @return ||pos-this||<sup>2</sup> = 4.sin<sup>2</sup>(r/2)
+   **/
+   dist2: function(pos) {
+//    	if ((this.x==0)&&(this.y==0)&&(this.z==0)) return(0./0.);
+//    	if ((pos.x==0)&&(pos.y==0)&&(pos.z==0)) return(0./0.);
+	var w = pos.x - this.x;
+	var r2 = w * w;
+	w = pos.y - this.y; r2 += w * w;
+	w = pos.z - this.z; r2 += w * w;
+	return r2;
+   },
+
+   /**
+    * Distance between 2 points on the sphere.
+    * @param  pos another position on the sphere
+    * @return distance in degrees in range [0, 180]
+   **/
+    distance: function(pos) {
+      // Take care of NaN:
+    	if ((pos.x==0)&&(pos.y==0)&&(pos.z==0)) return(0./0.);
+    	if ((this.x==0)&&(this.y==0)&&(this.z==0)) return(0./0.);
+      return (2. * AstroMath.asind(0.5 * Math.sqrt(this.dist2(pos))));
+    },
+
+   /**
+    * Transform the position into another frame.
+    * @param new_frame	The frame of the resulting position.
+   **/
+   convertTo: function(new_frame) {
+		// Verify first if frames identical -- then nothing to do !
+		if (this.frame.equals(new_frame)) {
+	    		return;
+		}
+
+		// Move via ICRS
+		this.frame.toICRS(this.coo);	// Position now in ICRS
+		new_frame.fromICRS(this.coo);	// Position now in new_frame
+		this.frame = new_frame;
+		this.lon = this.lat = 0./0.;	// Actual angles not recomputed
+   },
+
+    /**
+     * Rotate a coordinate (apply a rotation to the position).
+     * @param R [3][3] Rotation Matrix
+     */
+    rotate: function(R) {
+      var X, Y, Z;
+		if (R == Umatrix3) return;
+		X = R[0][0]*this.x + R[0][1]*this.y + R[0][2]*this.z;
+		Y = R[1][0]*this.x + R[1][1]*this.y + R[1][2]*this.z;
+		Z = R[2][0]*this.x + R[2][1]*this.y + R[2][2]*this.z;
+    	// this.set(X, Y, Z); Not necessary to compute positions each time.
+		this.x = X; this.y = Y; this.z = Z;
+		this.lon = this.lat = 0./0.;
+    },
+
+    /**
+     * Rotate a coordinate (apply a rotation to the position) in reverse direction.
+     * The method is the inverse of rotate.
+     * @param R [3][3] Rotation Matrix
+     */
+    rotate_1: function(R) {
+      var X, Y, Z;
+      if (R == Umatrix3) return;
+		X = R[0][0]*this.x + R[1][0]*this.y + R[2][0]*this.z;
+		Y = R[0][1]*this.x + R[1][1]*this.y + R[2][1]*this.z;
+		Z = R[0][2]*this.x + R[1][2]*this.y + R[2][2]*this.z;
+    	// this.set(X, Y, Z); Not necessary to compute positions each time.
+		this.x = X; this.y = Y; this.z = Z;
+		this.lon = this.lat = 0./0.;
+    },
+
+
+    /**
+     * Test equality of Coo.
+     * @param coo Second coordinate to compare with
+     * @return  True if the two coordinates are equal
+     */
+    equals: function(coo) {
+		return this.x == coo.x && this.y == coo.y && this.z == coo.z;
+    },
+
+	/**
+	 * parse a coordinate string. The coordinates can be in decimal or sexagesimal
+	 * @param str string to parse
+	 * @return true if the parsing succeded, false otherwise
+	 */
+	parse: function(str) {
+		var p = str.indexOf('+');
+		if (p < 0) p = str.indexOf('-');
+		if (p < 0) p = str.indexOf(' ');
+		if (p < 0) {
+			this.lon = 0.0/0.0;
+			this.lat = 0.0/0.0;
+			this.prec = 0;
+			return false;
+		}
+		var strlon = str.substring(0,p);
+		var strlat = str.substring(p);
+
+		this.lon = this.parseLon(strlon);	// sets the precision parameter
+		this.lat = this.parseLat(strlat);	// sets the precision parameter
+		return true;
+	},
+
+	parseLon: function(str) {
+		var str = str.trim();
+        str = str.replace(/:/g, ' ');
+
+		if (str.indexOf(' ') < 0) {
+			// The longitude is a integer or decimal number
+			var p = str.indexOf('.');
+			this.prec = p < 0 ? 0 : str.length - p - 1;
+			return parseFloat(str);
+		} else {
+			var stok = new Tokenizer(str,' ');
+			var i = 0;
+			var l = 0;
+			var pr = 0;
+			while (stok.hasMore()) {
+				var tok = stok.nextToken();
+				var dec = tok.indexOf('.');
+				l += parseFloat(tok)*Coo.factor[i];
+//				pr = dec < 0 ? 1 : 2;
+				switch (i) {
+					case 0: pr = dec < 0 ? 1 : 2; break;
+					case 1: pr = dec < 0 ? 3 : 4; break;
+					case 2: pr = dec < 0 ? 5 : 4+tok.length-dec;
+					default: break;
+				}
+				i++;
+			}
+			this.prec = pr;
+			return l*15/3600.0;
+		}
+	},
+
+	parseLat: function(str) {
+		var str = str.trim();
+        str = str.replace(/:/g, ' ');
+
+		var sign;
+		if (str.charAt(0) == '-') {
+			sign = -1;
+			str = str.substring(1);
+		} else if (str.charAt(0) == '-') {
+			sign = 1;
+			str = str.substring(1);
+		} else {
+			// No sign specified
+			sign = 1;
+		}
+		if (str.indexOf(' ') < 0) {
+			// The longitude is a integer or decimal number
+			var p = str.indexOf('.');
+			this.prec = p < 0 ? 0 : str.length - p - 1;
+			return parseFloat(str)*sign;
+		} else {
+			var stok = new Tokenizer(str,' ');
+			var i = 0;
+			var l = 0;
+			var pr = 0;
+			while (stok.hasMore()) {
+				var tok = stok.nextToken();
+				var dec = tok.indexOf('.');
+				l += parseFloat(tok)*Coo.factor[i];
+				switch (i) {
+					case 0: pr = dec < 0 ? 1 : 2; break;
+					case 1: pr = dec < 0 ? 3 : 4; break;
+					case 2: pr = dec < 0 ? 5 : 4+tok.length-dec;
+					default: break;
+				}
+				i++;
+			}
+			this.prec = pr;
+			return l*sign/3600.0;
+		}
+	},
+
+	/**
+	 * Format coordinates according to the options
+	 * @param options 'd': decimal, 's': sexagésimal, '/': space separated, '2': return [ra,dec] in an array
+	 * @return the formatted coordinates
+	 */
+	format: function(options) {
+		if (isNaN(this.lon)) this.computeLonLat();
+		var strlon = "", strlat = "";
+		if (options.indexOf('d') >= 0) {
+			// decimal display
+			strlon = Numbers.format(this.lon, this.prec);
+			strlat = Numbers.format(this.lat, this.prec);
+		} else {
+			// sexagesimal display
+			var hlon = this.lon/15.0;
+			var strlon = Numbers.toSexagesimal(hlon, this.prec+1, false);
+			var strlat = Numbers.toSexagesimal(this.lat, this.prec, false);
+		}
+		if (this.lat > 0) strlat = '+'+strlat;
+
+		if (options.indexOf('/') >= 0) {
+			return strlon+' '+strlat;
+		} else if (options.indexOf('2') >= 0) {
+			return [strlon, strlat];
+		}
+		return strlon+strlat;
+	}
+
+}
+
+/**
+ * Distance between 2 points on the sphere.
+ * @param coo1 firs	var coslat = AstroMath.cosd(this.lat);
+
+	this.x = coslat*AstroMath.cosd(this.lon);
+	this.y = coslat*AstroMath.sind(this.lon);
+	this.z = AstroMath.sind(this.lat);
+t coordinates point
+ * @param coo2 second coordinates point
+ * @return distance in degrees in range [0, 180]
+**/
+/*
+Coo.distance = function(Coo coo1, Coo coo2) {
+	return Coo.distance(coo1.lon, coo1.lat, coo2.lon, coo2.lat);
+}
+*/
+/**
+ * Distance between 2 points on the sphere.
+ * @param lon1 longitude of first point in degrees
+ * @param lat1 latitude of first point in degrees
+ * @param lon2 longitude of second point in degrees
+ * @param lat2 latitude of second point in degrees
+ * @return distance in degrees in range [0, 180]
+**/
+/*
+Coo.distance = function(lon1, lat1, lon2, lat2) {
+	var c1 = AstroMath.cosd(lat1);
+	var c2 = AstroMath.cosd(lat2);
+
+	var w, r2;
+	w = c1 * AstroMath.cosd(lon1) - c2 * AstroMath.cosd(lon2);
+	r2 = w*w;
+	w = c1 * AstroMath.sind(lon1) - c2 * AstroMath.sind(lon2);
+	r2 += w*w;
+	w = AstroMath.sind(lat1) - AstroMath.sind(lat2);
+	r2 += w*w;
+
+	return 2. * AstroMath.asind(0.5 * Math.sqrt(r2));
+}
+
+
+//===================================
+// Class Tokenizer (similar to Java)
+//===================================
+
+/**
+ * Constructor
+ * @param str String to tokenize
+ * @param sep token separator char
+ */
+function Tokenizer(str, sep) {
+	this.string = Strings.trim(str, sep);
+	this.sep = sep;
+	this.pos = 0;
+}
+
+Tokenizer.prototype = {
+	/**
+	 * Check if the string has more tokens
+	 * @return true if a token remains (read with nextToken())
+	 */
+	hasMore: function() {
+		return this.pos < this.string.length;
+	},
+
+	/**
+	 * Returns the next token (as long as hasMore() is true)
+	 * @return the token string
+	 */
+	nextToken: function() {
+		// skip all the separator chars
+		var p0 = this.pos;
+		while (p0 < this.string.length && this.string.charAt(p0) == this.sep) p0++;
+		var p1 = p0;
+		// get the token
+		while (p1 < this.string.length && this.string.charAt(p1) != this.sep) p1++;
+		this.pos = p1;
+		return this.string.substring(p0, p1);
+	},
+}
+
+//================================
+// Class Strings (static methods)
+//================================
+function Strings() {}
+
+/**
+ * Removes a given char at the beginning and the end of a string
+ * @param str string to trim
+ * @param c char to remove
+ * @return the trimmed string
+ */
+
+Strings.trim = function(str, c) {
+	var p0=0, p1=str.length-1;
+	while (p0 < str.length && str.charAt(p0) == c) p0++;
+	if (p0 == str.length) return "";
+	while (p1 > p0 && str.charAt(p1) == c) p1--;
+	return str.substring(p0, p1+1);
+}
+
+//================================
+// Class Numbers (static methods)
+//================================
+function Numbers() {}
+//                0  1   2    3     4      5       6        7         8          9
+Numbers.pow10 = [ 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000,
+//      10           11            12             13              14
+	10000000000, 100000000000, 1000000000000, 10000000000000, 100000000000000 ];
+//                 0    1     2      3       4        5         6          7
+Numbers.rndval = [ 0.5, 0.05, 0.005, 0.0005, 0.00005, 0.000005, 0.0000005, 0.00000005,
+//      8            9             10             11              12
+	0.000000005, 0.0000000005, 0.00000000005, 0.000000000005, 0.0000000000005,
+//      13                14
+	0.00000000000005, 0.00000000000005 ];
+/**
+ * Format a integer or decimal number, adjusting the value with 'prec' decimal digits
+ * @param num number (integer or decimal)
+ * @param prec precision (= number of decimal digit to keep or append)
+ * @return a string with the formatted number
+ */
+Numbers.format = function(num, prec) {
+		if (prec <= 0) {
+			// Return an integer number
+			return (Math.round(num)).toString();
+		}
+		var str = num.toString();
+		var p = str.indexOf('.');
+		var nbdec = p >= 0 ? str.length-p-1 : 0;
+		if (prec >= nbdec) {
+			if (p < 0) str += '.';
+			for (var i=0; i<prec-nbdec; i++)
+				str += '0';
+			return str;
+		}
+		// HERE: prec > 0 and prec < nbdec
+		str = (num+Numbers.rndval[prec]).toString();
+		return str.substr(0, p+prec+1);
+}
+
+
+/**
+ * Convert a decimal coordinate into sexagesimal string, according to the given precision<br>
+ * 8: 1/1000th sec, 7: 1/100th sec, 6: 1/10th sec, 5: sec, 4: 1/10th min, 3: min, 2: 1/10th deg, 1: deg
+ * @param num number (integer or decimal)
+ * @param prec precision (= number of decimal digit to keep or append)
+ * @param plus if true, the '+' sign is displayed
+ * @return a string with the formatted sexagesimal number
+ */
+Numbers.toSexagesimal = function(num, prec, plus) {
+	var resu = "";
+	var sign = num < 0 ? '-' : (plus ? '+' : '');
+	var n = Math.abs(num);
+
+	switch (prec) {
+		case 1:	// deg
+			var n1 = Math.round(n);
+			return sign+n1.toString();
+		case 2:	// deg.d
+			return sign+Numbers.format(n, 1);
+		case 3:	// deg min
+			var n1 = Math.floor(n);
+			var n2 = Math.round((n-n1)*60);
+			return sign+n1+' '+n2;
+		case 4:	// deg min.d
+			var n1 = Math.floor(n);
+			var n2 = (n-n1)*60;
+			return sign+n1+' '+Numbers.format(n2, 1);
+		case 5:	// deg min sec
+			var n1 = Math.floor(n);	// d
+			var n2 = (n-n1)*60;		// M.d
+			var n3 = Math.floor(n2);// M
+			var n4 = Math.round((n2-n3)*60);	// S
+			return sign+n1+' '+n3+' '+n4;
+		case 6:	// deg min sec.d
+		case 7:	// deg min sec.dd
+		case 8:	// deg min sec.ddd
+			var n1 = Math.floor(n);	// d
+			if (n1<10) n1 = '0' + n1;
+			var n2 = (n-n1)*60;		// M.d
+			var n3 = Math.floor(n2);// M
+			if (n3<10) n3 = '0' + n3;
+			var n4 = (n2-n3)*60;		// S.ddd
+			return sign+n1+' '+n3+' '+Numbers.format(n4, prec-5);
+		default:
+			return sign+Numbers.format(n, 1);
+	}
+}
+// Copyright 2018 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File SimbadPointer.js
+ *
+ * The SIMBAD pointer will query Simbad for a given position and radius and
+ * return information on the object with
+ *
+ * Author: Thomas Boch [CDS]
+ *
+ *****************************************************************************/
+
+SimbadPointer = (function() {
+
+
+    SimbadPointer = {};
+
+    SimbadPointer.MIRRORS = ['https://alasky.u-strasbg.fr/cgi/simbad-flat/simbad-quick.py', 'https://alaskybis.u-strasbg.fr/cgi/simbad-flat/simbad-quick.py']; // list of base URL for Simbad pointer service
+
+
+    SimbadPointer.query = function(ra, dec, radiusDegrees, aladinInstance) {
+        var coo = new Coo(ra, dec, 7);
+        var params = {Ident: coo.format('s/'), SR: radiusDegrees}
+        var successCallback = function(result) {
+            aladinInstance.view.setCursor('pointer');
+
+            var regexp = /(.*?)\/(.*?)\((.*?),(.*?)\)/g;
+            var match = regexp.exec(result);
+            if (match) {
+                var objCoo = new Coo();
+                objCoo.parse(match[1]);
+                var objName = match[2];
+                var title = '<div class="aladin-sp-title"><a target="_blank" href="http://simbad.u-strasbg.fr/simbad/sim-id?Ident=' + encodeURIComponent(objName) + '">' + objName + '</a></div>';
+                var content = '<div class="aladin-sp-content">';
+                content += '<em>Type: </em>' + match[4] + '<br>';
+                var magnitude = match[3];
+                if (Utils.isNumber(magnitude)) {
+                    content += '<em>Mag: </em>' + magnitude + '<br>';
+                }
+                content += '<br><a target="_blank" href="http://cdsportal.u-strasbg.fr/?target=' + encodeURIComponent(objName) + '">Query in CDS portal</a>';
+                content += '</div>';
+                aladinInstance.showPopup(objCoo.lon, objCoo.lat, title, content);
+            }
+            else {
+                aladinInstance.hidePopup();
+            }
+        };
+        var failureCallback = function() {
+            aladinInstance.view.setCursor('pointer');
+            aladinInstance.hidePopup();
+        };
+        Utils.loadFromMirrors(SimbadPointer.MIRRORS, {data: params, onSuccess: successCallback, onFailure: failureCallback, timeout: 5});
+
+    };
+
+    return SimbadPointer;
+})();
+
+// Copyright 2013-2017 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File Box
+ *
+ * A Box instance is a GUI element providing a div nested
+ * in Aladin Lite parent div
+ *
+ * Author: Thomas Boch [CDS]
+ *
+ *****************************************************************************/
+Box = (function() {
+
+    // constructor
+    var Box = function(properties) {
+
+        this.$parentDiv = $('<div>');
+        this.$parentDiv.addClass('aladin-box');
+
+        properties = properties || {};
+
+        this.css = properties.css || {padding: '4px'};
+
+        this.position = properties.position || 'bottom'; // position can be bottom, left, top or right
+        if (this.position=='right') {
+            this.css['left'] = 'unset';
+        }
+        this.css[this.position] = '4px';
+
+        this.contentCss = properties.contentCss || {};
+
+        this.title = properties.title || undefined;
+
+        this.content = properties.content || undefined;
+
+        this.showHandler = properties.showHandler !== undefined ? properties.showHandler : true;
+
+        this.openCallback = properties.openCallback || undefined; // callback called when the user opens the panel
+        this.closeCallback = properties.closeCallback || undefined; // callback called when the user closes the panel
+
+        this.changingDim = 'width';
+        if (this.position=='top' || this.position=='bottom') {
+            this.changingDim = 'height';
+        }
+
+
+        this.open = false;
+        this._render();
+        this.$parentDiv.show();
+        this.open = true;
+        this.hide();
+    };
+
+    Box.prototype = {
+
+        show: function() {
+            if (this.open) {
+                return;
+            }
+
+            this.open = true;
+            this.$parentDiv.show();
+            this._updateChevron();
+
+            if (this.changingDim=='width') {
+                this.$parentDiv.find('.aladin-box-title-label').show();
+            }
+            var self = this;
+            var options = {};
+            options[this.changingDim] = 'show';
+            var delay = this.changingDim=='width' ? 0 : 400;
+            this.$parentDiv.find('.aladin-box-content').animate(options, delay, function() {
+                self.css[self.position] = '4px';
+                self.updateStyle(self.css);
+
+                typeof self.openCallback === 'function' && self.openCallback();
+            });
+
+        },
+
+        hide: function() {
+            if (! this.open) {
+                return;
+            }
+
+            this.open = false;
+            this._updateChevron();
+
+            if (this.changingDim=='width') {
+                this.$parentDiv.find('.aladin-box-title-label').hide();
+            }
+            var self = this;
+            var options = {};
+            options[this.changingDim] = 'hide';
+            var delay = this.changingDim=='width' ? 0 : 400;
+            this.$parentDiv.find('.aladin-box-content').animate(options, delay, function() {
+                self.css[self.position] = '0px';
+                self.updateStyle(self.css);
+
+                typeof self.closeCallback === 'function' && self.closeCallback();
+            });
+        },
+
+        // complety hide parent div
+        realHide: function() {
+            this.open = false;
+            this.$parentDiv.hide();
+        },
+
+        updateStyle: function(css) {
+            this.css = css;
+            this.$parentDiv.css(css);
+        },
+
+        setContent: function(content) {
+            this.content = content;
+            this._render();
+        },
+
+        setTitle: function(title) {
+            this.title = title;
+            this._render();
+        },
+
+        enable: function() {
+            this.$parentDiv.enable();
+        },
+
+        disable: function() {
+            this.$parentDiv.disable();
+        },
+
+        // fill $parentDiv with HTML corresponding to current state
+        _render: function() {
+            var self = this;
+
+            this.$parentDiv.empty();
+            this.$parentDiv.off();
+
+            var titleDiv = $('<div class="aladin-box-title">');
+            if (this.showHandler) {
+                var chevron = $('<span class="aladin-chevron">');
+                titleDiv.append(chevron);
+            }
+            if (this.title) {
+                titleDiv.append(' <span class="aladin-box-title-label">' + this.title + '</span>');
+            }
+            this.$parentDiv.append(titleDiv);
+            var $content = $('<div class="aladin-box-content">' + (this.content?this.content:'') + '</div>');
+            $content.css(this.contentCss);
+            this.$parentDiv.append($content);
+
+            this._updateChevron();
+            this.updateStyle(this.css);
+
+            titleDiv.on('click', function() {
+                if (self.open) {
+                    self.hide();
+                }
+                else {
+                    self.show();
+                }
+            });
+        },
+
+        _updateChevron: function() {
+            this.$parentDiv.find('.aladin-chevron').removeClass().addClass('aladin-chevron ' + getChevronClass(this.position, this.open))
+                                                        .attr('title', 'Click to ' + (this.open?'hide ':'show ') + (this.title?this.title:'') + ' panel');
+        }
+    };
+
+    // return the jquery object corresponding to the given position and open/close state
+    var getChevronClass = function(position, isOpen) {
+        if (position=='top' && isOpen || position=='bottom' && !isOpen) {
+            return 'aladin-chevron-up';
+        }
+        if (position=='bottom' && isOpen || position=='top' && !isOpen) {
+            return 'aladin-chevron-down';
+        }
+        if (position=='right' && isOpen || position=='left' && !isOpen) {
+            return 'aladin-chevron-right';
+        }
+        if (position=='left' && isOpen || position=='right' && !isOpen) {
+            return 'aladin-chevron-left';
+        }
+        return '';
+    };
+
+
+
+
+    return Box;
+
+})();
+
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+CooConversion = (function() {
+
+    var CooConversion = {};
+
+    CooConversion.GALACTIC_TO_J2000 = [
+       -0.0548755604024359,  0.4941094279435681, -0.8676661489811610,
+       -0.8734370902479237, -0.4448296299195045, -0.1980763734646737,
+       -0.4838350155267381,  0.7469822444763707,  0.4559837762325372 ];
+
+    CooConversion.J2000_TO_GALACTIC = [
+        -0.0548755604024359, -0.873437090247923, -0.4838350155267381,
+         0.4941094279435681, -0.4448296299195045, 0.7469822444763707,
+        -0.8676661489811610, -0.1980763734646737, 0.4559837762325372 ];
+
+    // adapted from www.robertmartinayers.org/tools/coordinates.html
+    // radec : array of ra, dec in degrees
+    // return coo in degrees
+    CooConversion.Transform = function( radec, matrix ) {// returns a radec array of two elements
+        radec[0] = radec[0]*Math.PI/180;
+        radec[1] = radec[1]*Math.PI/180;
+      var r0 = new Array (
+       Math.cos(radec[0]) * Math.cos(radec[1]),
+       Math.sin(radec[0]) * Math.cos(radec[1]),
+       Math.sin(radec[1]) );
+
+     var s0 = new Array (
+       r0[0]*matrix[0] + r0[1]*matrix[1] + r0[2]*matrix[2],
+       r0[0]*matrix[3] + r0[1]*matrix[4] + r0[2]*matrix[5],
+       r0[0]*matrix[6] + r0[1]*matrix[7] + r0[2]*matrix[8] );
+
+      var r = Math.sqrt ( s0[0]*s0[0] + s0[1]*s0[1] + s0[2]*s0[2] );
+
+      var result = new Array ( 0.0, 0.0 );
+      result[1] = Math.asin ( s0[2]/r ); // New dec in range -90.0 -- +90.0
+      // or use sin^2 + cos^2 = 1.0
+      var cosaa = ( (s0[0]/r) / Math.cos(result[1] ) );
+      var sinaa = ( (s0[1]/r) / Math.cos(result[1] ) );
+      result[0] = Math.atan2 (sinaa,cosaa);
+      if ( result[0] < 0.0 ) result[0] = result[0] + 2*Math.PI;
+
+        result[0] = result[0]*180/Math.PI;
+        result[1] = result[1]*180/Math.PI;
+      return result;
+    };
+
+    // coo : array of lon, lat in degrees
+    CooConversion.GalacticToJ2000 = function(coo) {
+        return CooConversion.Transform(coo, CooConversion.GALACTIC_TO_J2000);
+    };
+    // coo : array of lon, lat in degrees
+    CooConversion.J2000ToGalactic = function(coo) {
+        return CooConversion.Transform(coo, CooConversion.J2000_TO_GALACTIC);
+    };
+    return CooConversion;
+})();
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File Sesame.js
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+Sesame = (function() {
+    Sesame = {};
+
+    Sesame.cache = {};
+
+    Sesame.SESAME_URL = "http://cds.u-strasbg.fr/cgi-bin/nph-sesame.jsonp";
+
+    /** find RA, DEC for any target (object name or position)
+     *  if successful, callback is called with an object {ra: <ra-value>, dec: <dec-value>}
+     *  if not successful, errorCallback is called
+     */
+    Sesame.getTargetRADec = function(target, callback, errorCallback) {
+        if (!callback) {
+            return;
+        }
+        var isObjectName = /[a-zA-Z]/.test(target);
+
+        // try to parse as a position
+        if ( ! isObjectName) {
+            var coo = new Coo();
+
+            coo.parse(target);
+            if (callback) {
+                callback({ra: coo.lon, dec: coo.lat});
+            }
+        }
+        // ask resolution by Sesame
+        else {
+            Sesame.resolve(target,
+                   function(data) { // success callback
+                       callback({ra:  data.Target.Resolver.jradeg,
+                                 dec: data.Target.Resolver.jdedeg});
+                   },
+
+                   function(data) { // error callback
+                       if (errorCallback) {
+                           errorCallback();
+                       }
+                   }
+           );
+        }
+    };
+
+    Sesame.resolve = function(objectName, callbackFunctionSuccess, callbackFunctionError) {
+        var sesameUrl = Sesame.SESAME_URL;
+        if (Utils.isHttpsContext()) {
+            sesameUrl = sesameUrl.replace('http://', 'https://')
+        }
+
+
+        $.ajax({
+            url: sesameUrl ,
+            data: {"object": objectName},
+            method: 'GET',
+            dataType: 'jsonp',
+            success: function(data) {
+                if (data.Target && data.Target.Resolver && data.Target.Resolver) {
+                    callbackFunctionSuccess(data);
+                }
+                else {
+                    callbackFunctionError(data);
+                }
+            },
+            error: callbackFunctionError
+            });
+    };
+
+    return Sesame;
+})();
+
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File HealpixCache
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+// class holding some HEALPix computations for better performances
+//
+// it is made of :
+// - a static cache for HEALPix corners at nside=8
+// - a dynamic cache for
+HealpixCache = (function() {
+
+    var HealpixCache = {};
+
+    HealpixCache.staticCache = {corners: {nside8: []}};
+    // TODO : utilisation du dynamicCache
+    HealpixCache.dynamicCache = {};
+
+    HealpixCache.lastNside = 8;
+
+    HealpixCache.hpxIdxCache = null;
+
+    // TODO : conserver en cache le dernier résultat ?
+
+    HealpixCache.init = function() {
+    	// pre-compute corners position for nside=8
+    	var hpxIdx = new HealpixIndex(8);
+    	hpxIdx.init();
+    	var npix = HealpixIndex.nside2Npix(8);
+        var corners;
+    	for (var ipix=0; ipix<npix; ipix++) {
+            corners =  hpxIdx.corners_nest(ipix, 1);
+    		HealpixCache.staticCache.corners.nside8.push(corners);
+    	}
+
+    	HealpixCache.hpxIdxCache = hpxIdx;
+    };
+
+    HealpixCache.init();
+
+    HealpixCache.corners_nest = function(ipix, nside) {
+    	if (nside==8) {
+    		return HealpixCache.staticCache.corners.nside8[ipix];
+    	}
+
+    	if (nside != HealpixCache.lastNside) {
+    		HealpixCache.hpxIdxCache = new HealpixIndex(nside);
+    		HealpixCache.hpxIdxCache.init();
+    		HealpixCache.lastNside = nside;
+    	}
+
+    	return HealpixCache.hpxIdxCache.corners_nest(ipix, 1);
+
+    };
+
+    return HealpixCache;
+})();
+
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File Utils
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+Utils = Utils || {};
+
+Utils.cssScale = undefined;
+// adding relMouseCoords to HTMLCanvasElement prototype (see http://stackoverflow.com/questions/55677/how-do-i-get-the-coordinates-of-a-mouse-click-on-a-canvas-element )
+function relMouseCoords(event) {
+    var totalOffsetX = 0;
+    var totalOffsetY = 0;
+    var canvasX = 0;
+    var canvasY = 0;
+    var currentElement = this;
+
+    if (event.offsetX) {
+        return {x: event.offsetX, y:event.offsetY};
+    }
+    else {
+        if (!Utils.cssScale) {
+            var st = window.getComputedStyle(document.body, null);
+            var tr = st.getPropertyValue("-webkit-transform") ||
+                    st.getPropertyValue("-moz-transform") ||
+                    st.getPropertyValue("-ms-transform") ||
+                    st.getPropertyValue("-o-transform") ||
+                    st.getPropertyValue("transform");
+            var matrixRegex = /matrix\((-?\d*\.?\d+),\s*0,\s*0,\s*(-?\d*\.?\d+),\s*0,\s*0\)/;
+            var matches = tr.match(matrixRegex);
+            if (matches) {
+                Utils.cssScale = parseFloat(matches[1]);
+            }
+            else {
+                Utils.cssScale = 1;
+            }
+        }
+        var e = event;
+        var canvas = e.target;
+        // http://www.jacklmoore.com/notes/mouse-position/
+        var target = e.target || e.srcElement;
+        var style = target.currentStyle || window.getComputedStyle(target, null);
+        var borderLeftWidth = parseInt(style['borderLeftWidth'], 10);
+        var borderTopWidth = parseInt(style['borderTopWidth'], 10);
+        var rect = target.getBoundingClientRect();
+
+        var clientX = e.clientX;
+        var clientY = e.clientY;
+        if (e.clientX == undefined) {
+            clientX = e.originalEvent.changedTouches[0].clientX;
+            clientY = e.originalEvent.changedTouches[0].clientY;
+        }
+
+        var offsetX = clientX - borderLeftWidth - rect.left;
+        var offsetY = clientY - borderTopWidth - rect.top
+
+        return {x: parseInt(offsetX/Utils.cssScale), y: parseInt(offsetY/Utils.cssScale)};
+    }
+}
+HTMLCanvasElement.prototype.relMouseCoords = relMouseCoords;
+
+
+
+//Function.prototype.bind polyfill from
+//https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
+if (!Function.prototype.bind) {
+    Function.prototype.bind = function (obj) {
+        // closest thing possible to the ECMAScript 5 internal IsCallable function
+        if (typeof this !== 'function') {
+            throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
+        }
+
+        var slice = [].slice,
+        args = slice.call(arguments, 1),
+        self = this,
+        nop = function () { },
+        bound = function () {
+            return self.apply(this instanceof nop ? this : (obj || {}),
+                    args.concat(slice.call(arguments)));
+        };
+
+        bound.prototype = this.prototype;
+
+        return bound;
+    };
+}
+
+
+
+
+
+
+
+
+$ = $ || jQuery;
+
+/* source : http://stackoverflow.com/a/8764051 */
+$.urlParam = function(name, queryString){
+    if (queryString===undefined) {
+        queryString = location.search;
+    }
+	return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(queryString)||[,""])[1].replace(/\+/g, '%20'))||null;
+};
+
+/* source: http://stackoverflow.com/a/1830844 */
+Utils.isNumber = function(n) {
+  return !isNaN(parseFloat(n)) && isFinite(n);
+};
+
+Utils.isInt = function(n) {
+    return Utils.isNumber(n) && Math.floor(n)==n;
+};
+
+/* a debounce function, used to prevent multiple calls to the same function if less than delay milliseconds have passed */
+Utils.debounce = function(fn, delay) {
+    var timer = null;
+    return function () {
+      var context = this, args = arguments;
+      clearTimeout(timer);
+      timer = setTimeout(function () {
+        fn.apply(context, args);
+      }, delay);
+    };
+};
+
+/* return a throttled function, to rate limit the number of calls (by default, one call every 250 milliseconds) */
+Utils.throttle = function(fn, threshhold, scope) {
+  threshhold || (threshhold = 250);
+  var last,
+      deferTimer;
+  return function () {
+    var context = scope || this;
+
+    var now = +new Date,
+        args = arguments;
+    if (last && now < last + threshhold) {
+      // hold on to it
+      clearTimeout(deferTimer);
+      deferTimer = setTimeout(function () {
+        last = now;
+        fn.apply(context, args);
+      }, threshhold);
+    } else {
+      last = now;
+      fn.apply(context, args);
+    }
+  };
+}
+
+
+/* A LRU cache, inspired by https://gist.github.com/devinus/409353#file-gistfile1-js */
+// TODO : utiliser le LRU cache pour les tuiles images
+Utils.LRUCache = function (maxsize) {
+    this._keys = [];
+    this._items = {};
+    this._expires = {};
+    this._size = 0;
+    this._maxsize = maxsize || 1024;
+};
+
+Utils.LRUCache.prototype = {
+        set: function (key, value) {
+            var keys = this._keys,
+                items = this._items,
+                expires = this._expires,
+                size = this._size,
+                maxsize = this._maxsize;
+
+            if (size >= maxsize) { // remove oldest element when no more room
+                keys.sort(function (a, b) {
+                    if (expires[a] > expires[b]) return -1;
+                    if (expires[a] < expires[b]) return 1;
+                    return 0;
+                });
+
+                size--;
+                delete expires[keys[size]];
+                delete items[keys[size]];
+            }
+
+            keys[size] = key;
+            items[key] = value;
+            expires[key] = Date.now();
+            size++;
+
+            this._keys = keys;
+            this._items = items;
+            this._expires = expires;
+            this._size = size;
+        },
+
+        get: function (key) {
+            var item = this._items[key];
+            if (item) this._expires[key] = Date.now();
+            return item;
+        },
+
+        keys: function() {
+            return this._keys;
+        }
+};
+
+////////////////////////////////////////////////////////////////////////////:
+
+/**
+  Make an AJAX call, given a list of potential mirrors
+  First successful call will result in options.onSuccess being called back
+  If all calls fail, onFailure is called back at the end
+
+  This method assumes the URL are CORS-compatible, no proxy will be used
+ */
+Utils.loadFromMirrors = function(urls, options) {
+    var data    = options && options.data || null;
+    var method = options && options.method || 'GET';
+    var dataType = options && options.dataType || null;
+    var timeout = options && options.timeout || 20;
+
+    var onSuccess = options && options.onSuccess || null;
+    var onFailure = options && options.onFailure || null;
+
+    if (urls.length === 0) {
+        (typeof onFailure === 'function') && onFailure();
+    }
+    else {
+        var ajaxOptions = {
+            url: urls[0],
+            data: data
+        }
+        if (dataType) {
+            ajaxOptions.dataType = dataType;
+        }
+
+        $.ajax(ajaxOptions)
+        .done(function(data) {
+            (typeof onSuccess === 'function') && onSuccess(data);
+        })
+        .fail(function() {
+             Utils.loadFromMirrors(urls.slice(1), options);
+        });
+    }
+}
+
+// return the jquery ajax object configured with the requested parameters
+// by default, we use the proxy (safer, as we don't know if the remote server supports CORS)
+Utils.getAjaxObject = function(url, method, dataType, useProxy) {
+        if (useProxy!==false) {
+            useProxy = true;
+        }
+
+        if (useProxy===true) {
+            var urlToRequest = Aladin.JSONP_PROXY + '?url=' + encodeURIComponent(url);
+        }
+        else {
+            urlToRequest = url;
+        }
+        method = method || 'GET';
+        dataType = dataType || null;
+
+        return $.ajax({
+            url: urlToRequest,
+            method: method,
+            dataType: dataType
+        });
+};
+
+// return true if script is executed in a HTTPS context
+// return false otherwise
+Utils.isHttpsContext = function() {
+    return ( window.location.protocol === 'https:' );
+};
+
+// generate an absolute URL from a relative URL
+// example: getAbsoluteURL('foo/bar/toto') return http://cds.unistra.fr/AL/foo/bar/toto if executed from page http://cds.unistra.fr/AL/
+Utils.getAbsoluteURL = function(url) {
+    var a = document.createElement('a');
+    a.href = url;
+
+    return a.href;
+};
+
+// generate a valid v4 UUID
+Utils.uuidv4 = function() {
+    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+        var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+        return v.toString(16);
+    });
+}
+
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File URLBuilder
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+
+URLBuilder = (function() {
+
+    URLBuilder = {
+        buildSimbadCSURL: function(target, radiusDegrees) {
+            if (target && (typeof target  === "object")) {
+                if ('ra' in target && 'dec' in target) {
+                    var coo = new Coo(target.ra, target.dec, 7);
+                    target = coo.format('s');
+                }
+            }
+            return 'https://alasky.unistra.fr/cgi/simbad-flat/simbad-cs.py?target=' + encodeURIComponent(target) + '&SR=' + radiusDegrees + '&format=votable&SRUNIT=deg&SORTBY=nbref';
+        },
+
+        buildNEDPositionCSURL: function(ra, dec, radiusDegrees) {
+                return 'https://ned.ipac.caltech.edu/cgi-bin/nph-objsearch?search_type=Near+Position+Search&of=xml_main&RA=' + ra + '&DEC=' + dec + '&SR=' + radiusDegrees;
+        },
+
+        buildNEDObjectCSURL: function(object, radiusDegrees) {
+                return 'https://ned.ipac.caltech.edu/cgi-bin/nph-objsearch?search_type=Near+Name+Search&radius=' + (60 * radiusDegrees) + '&of=xml_main&objname=' + object;
+        },
+
+        buildVizieRCSURL: function(vizCatId, target, radiusDegrees, options) {
+            if (target && (typeof target  === "object")) {
+                if ('ra' in target && 'dec' in target) {
+                    var coo = new Coo(target.ra, target.dec, 7);
+                    target = coo.format('s');
+                }
+            }
+
+            var maxNbSources = 1e5;
+            if (options && options.hasOwnProperty('limit') && Utils.isNumber(options.limit)) {
+                maxNbSources = parseInt(options.limit);
+            }
+            return 'https://vizier.unistra.fr/viz-bin/votable?-source=' + vizCatId + '&-c=' + encodeURIComponent(target) + '&-out.max=' + maxNbSources + '&-c.rd=' + radiusDegrees;
+        },
+
+        buildSkyBotCSURL: function(ra, dec, radius, epoch, queryOptions) {
+            var url = 'http://vo.imcce.fr/webservices/skybot/skybotconesearch_query.php?-from=AladinLite';
+            url += '&RA=' + encodeURIComponent(ra);
+            url += '&DEC=' + encodeURIComponent(dec);
+            url += '&SR=' + encodeURIComponent(radius);
+            url += '&EPOCH=' + encodeURIComponent(epoch);
+
+            if (queryOptions) {
+                for (var key in queryOptions) {
+                    if (queryOptions.hasOwnProperty(key)) {
+                            url += '&' + key + '=' + encodeURIComponent(queryOptions[key]);
+                    }
+                }
+            }
+
+            return url;
+        }
+
+
+    };
+
+    return URLBuilder;
+
+})();
+
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File MeasurementTable
+ *
+ * Graphic object showing measurement of a catalog
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+MeasurementTable = (function() {
+
+
+    // constructor
+    MeasurementTable = function(aladinLiteDiv) {
+        this.isShowing = false;
+
+        this.divEl = $('<div class="aladin-measurement-div"></div>');
+
+        $(aladinLiteDiv).append(this.divEl);
+    }
+
+    // show measurement associated with a given source
+    MeasurementTable.prototype.showMeasurement = function(source) {
+        this.divEl.empty();
+        var header = '<thead><tr>';
+        var content = '<tr>';
+        for (key in source.data) {
+            header += '<th>' + key + '</th>';
+            content += '<td>' + source.data[key] + '</td>';
+        }
+        header += '</tr></thead>';
+        content += '</tr>';
+        this.divEl.append('<table>' + header + content + '</table>');
+        this.show();
+    };
+
+    MeasurementTable.prototype.show = function() {
+        this.divEl.show();
+    };
+
+    MeasurementTable.prototype.hide = function() {
+        this.divEl.hide();
+    };
+
+
+    return MeasurementTable;
+})();
+
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File Color
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+Color = (function() {
+
+
+    Color = {};
+
+    Color.curIdx = 0;
+    Color.colors = ['#ff0000', '#0000ff', '#99cc00', '#ffff00','#000066', '#00ffff', '#9900cc', '#0099cc', '#cc9900', '#cc0099', '#00cc99', '#663333', '#ffcc9a', '#ff9acc', '#ccff33', '#660000', '#ffcc33', '#ff00ff', '#00ff00', '#ffffff'];
+
+
+    Color.getNextColor = function() {
+        var c = Color.colors[Color.curIdx % (Color.colors.length)];
+        Color.curIdx++;
+        return c;
+    };
+
+    /** return most suited (ie readable) color for a label, given a background color
+     * bkgdColor: color, given as a 'rgb(<r value>, <g value>, <v value>)' . This is returned by $(<element>).css('background-color')
+     *
+     * example call: Color.getLabelColorForBackground('rgb(3, 123, 42)')
+     * adapted from http://stackoverflow.com/questions/1855884/determine-font-color-based-on-background-color
+     */
+    Color.getLabelColorForBackground = function(rgbBkgdColor) {
+        var lightLabel = '#eee'
+        var darkLabel = '#111'
+        rgb = rgbBkgdColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
+        if (rgb==null) {
+            // we return the dark label color if we can't parse the color
+            return darkLabel
+        }
+        r = parseInt(rgb[1]);
+        g = parseInt(rgb[2]);
+        b = parseInt(rgb[3]);
+
+        var d = 0;
+        // Counting the perceptive luminance - human eye favors green color...
+        var a = 1 - ( 0.299 * r + 0.587 * g + 0.114 * b) / 255;
+
+        if (a < 0.5) {
+            return darkLabel; // bright color --> dark font
+        }
+        else {
+            return lightLabel; // dark color --> light font
+        }
+    };
+
+    return Color;
+})();
+
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File AladinUtils
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+AladinUtils = (function() {
+
+    return {
+    	/**
+    	 * passage de xy projection à xy dans la vue écran
+    	 * @param x
+    	 * @param y
+    	 * @param width
+    	 * @param height
+    	 * @param largestDim largest dimension of the view
+    	 * @returns position in the view
+    	 */
+    	xyToView: function(x, y, width, height, largestDim, zoomFactor, round) {
+    	    if (round==undefined) {
+                // we round by default
+    	        round = true;
+    	    }
+
+    	    if (round) {
+    	        // we round the result for potential performance gains
+    	        return {vx: AladinUtils.myRound(largestDim/2*(1+zoomFactor*x)-(largestDim-width)/2), vy: AladinUtils.myRound(largestDim/2*(1+zoomFactor*y)-(largestDim-height)/2)};
+
+    	    }
+    	    else {
+                return {vx: largestDim/2*(1+zoomFactor*x)-(largestDim-width)/2, vy: largestDim/2*(1+zoomFactor*y)-(largestDim-height)/2};
+    	    }
+    	},
+
+    	/**
+    	 * passage de xy dans la vue écran à xy projection
+    	 * @param vx
+    	 * @param vy
+    	 * @param width
+    	 * @param height
+    	 * @param largestDim
+    	 * @param zoomFactor
+    	 * @returns position in xy projection
+    	 */
+    	viewToXy: function(vx, vy, width, height, largestDim, zoomFactor) {
+    		return {x: ((2*vx+(largestDim-width))/largestDim-1)/zoomFactor, y: ((2*vy+(largestDim-height))/largestDim-1)/zoomFactor};
+    	},
+
+    	/**
+    	 * convert a
+    	 * @returns position x,y in the view. Null if projection is impossible
+    	 */
+        radecToViewXy: function(ra, dec, currentProjection, currentFrame, width, height, largestDim, zoomFactor) {
+            var xy;
+            if (currentFrame.system != CooFrameEnum.SYSTEMS.J2000) {
+                var lonlat = CooConversion.J2000ToGalactic([ra, dec]);
+                xy = currentProjection.project(lonlat[0], lonlat[1]);
+            }
+            else {
+                xy = currentProjection.project(ra, dec);
+            }
+            if (!xy) {
+                return null;
+            }
+
+            return AladinUtils.xyToView(xy.X, xy.Y, width, height, largestDim, zoomFactor, false);
+        },
+
+
+    	myRound: function(a) {
+    		if (a<0) {
+    			return -1*( (-a) | 0);
+    		}
+    		else {
+    			return a | 0;
+    		}
+    	},
+
+
+
+    	/**
+    	 * tests whether a healpix pixel is visible or not
+    	 * @param pixCorners array of position (xy view) of the corners of the pixel
+    	 * @param viewW
+    	 */
+    	isHpxPixVisible: function(pixCorners, viewWidth, viewHeight) {
+    		for (var i = 0; i<pixCorners.length; i++) {
+    			if ( pixCorners[i].vx>=-20 && pixCorners[i].vx<(viewWidth+20) &&
+    				 pixCorners[i].vy>=-20 && pixCorners[i].vy<(viewHeight+20) ) {
+    				return true;
+    			}
+    		}
+    		return false;
+    	},
+
+    	ipixToIpix: function(npixIn, norderIn, norderOut) {
+    		var npixOut = [];
+    		if (norderIn>=norderOut) {
+    		}
+    	},
+
+        getZoomFactorForAngle: function(angleInDegrees, projectionMethod) {
+            var p1 = {ra: 0, dec: 0};
+            var p2 = {ra: angleInDegrees, dec: 0};
+            var projection = new Projection(angleInDegrees/2, 0);
+            projection.setProjection(projectionMethod);
+            var p1Projected = projection.project(p1.ra, p1.dec);
+            var p2Projected = projection.project(p2.ra, p2.dec);
+
+            var zoomFactor = 1/Math.abs(p1Projected.X - p2Projected.Y);
+
+            return zoomFactor;
+        },
+
+        // grow array b of vx,vy view positions by *val* pixels
+        grow2: function(b, val) {
+            var j=0;
+            for ( var i=0; i<4; i++ ) {
+                if ( b[i]==null ) {
+                    j++;
+                }
+            }
+
+            if( j>1 ) {
+                return b;
+            }
+
+            var b1 = [];
+            for ( var i=0; i<4; i++ ) {
+                b1.push( {vx: b[i].vx, vy: b[i].vy} );
+            }
+
+            for ( var i=0; i<2; i++ ) {
+                var a = i==1 ? 1 : 0;
+                var c = i==1 ? 3 : 2;
+
+                if ( b1[a]==null ) {
+                    var d,g;
+                    if ( a==0 || a==3 ) {
+                        d=1;
+                        g=2;
+                    }
+                    else {
+                        d=0;
+                        g=3;
+                    }
+                    b1[a] = {vx: (b1[d].vx+b1[g].vx)/2, vy: (b1[d].vy+b1[g].vy)/2};
+                }
+                if ( b1[c]==null ) {
+                    var d,g;
+                    if ( c==0 || c==3 ) {
+                        d=1;
+                        g=2;
+                    }
+                    else {
+                        d=0;
+                        g=3;
+                    }
+                    b1[c] = {vx: (b1[d].vx+b1[g].vx)/2, vy: (b1[d].vy+b1[g].vy)/2};
+                }
+                if( b1[a]==null || b1[c]==null ) {
+                    continue;
+                }
+
+                var angle = Math.atan2(b1[c].vy-b1[a].vy, b1[c].vx-b1[a].vx);
+                var chouilla = val*Math.cos(angle);
+                b1[a].vx -= chouilla;
+                b1[c].vx += chouilla;
+                chouilla = val*Math.sin(angle);
+                b1[a].vy-=chouilla;
+                b1[c].vy+=chouilla;
+            }
+            return b1;
+        },
+
+        // SVG icons templates are stored here rather than in a CSS, as to allow
+        // to dynamically change the fill color
+        // Pretty ugly, haven't found a prettier solution yet
+        //
+        // TODO: store this in the Stack class once it will exist
+        //
+        SVG_ICONS: {
+            CATALOG: '<svg xmlns="http://www.w3.org/2000/svg"><polygon points="1,0,5,0,5,3,1,3"  fill="FILLCOLOR" /><polygon points="7,0,9,0,9,3,7,3"  fill="FILLCOLOR" /><polygon points="10,0,12,0,12,3,10,3"  fill="FILLCOLOR" /><polygon points="13,0,15,0,15,3,13,3"  fill="FILLCOLOR" /><polyline points="1,5,5,9"  stroke="FILLCOLOR" /><polyline points="1,9,5,5" stroke="FILLCOLOR" /><line x1="7" y1="7" x2="15" y2="7" stroke="FILLCOLOR" stroke-width="2" /><polyline points="1,11,5,15"  stroke="FILLCOLOR" /><polyline points="1,15,5,11"  stroke="FILLCOLOR" /><line x1="7" y1="13" x2="15" y2="13" stroke="FILLCOLOR" stroke-width="2" /></svg>',
+            MOC: '<svg xmlns="http://www.w3.org/2000/svg"><polyline points="0.5,7,2.5,7,2.5,5,7,5,7,3,10,3,10,5,13,5,13,7,15,7,15,9,13,9,13,12,10,12,10,14,7,14,7,12,2.5,12,2.5,10,0.5,10,0.5,7" stroke-width="1" stroke="FILLCOLOR" fill="transparent" /><line x1="1" y1="10" x2="6" y2="5" stroke="FILLCOLOR" stroke-width="0.5" /><line x1="2" y1="12" x2="10" y2="4" stroke="FILLCOLOR" stroke-width="0.5" /><line x1="5" y1="12" x2="12" y2="5" stroke="FILLCOLOR" stroke-width="0.5" /><line x1="7" y1="13" x2="13" y2="7" stroke="FILLCOLOR" stroke-width="0.5" /><line x1="10" y1="13" x2="13" y2="10" stroke="FILLCOLOR" stroke-width="0.5" /></svg>',
+            OVERLAY: '<svg xmlns="http://www.w3.org/2000/svg"><polygon points="10,5,10,1,14,1,14,14,2,14,2,9,6,9,6,5" fill="transparent" stroke="FILLCOLOR" stroke-width="2"/></svg>'
+        }
+
+    };
+
+})();
+
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File CooFrameEnum
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+ ProjectionEnum = {
+    SIN: Projection.PROJ_SIN,
+    AITOFF:  Projection.PROJ_AITOFF
+ };
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File CooFrameEnum
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+CooFrameEnum = (function() {
+
+    var systems = {J2000: 'J2000', GAL: 'Galactic'};
+    return {
+        SYSTEMS: systems,
+
+        J2000: {label: "J2000", system: systems.J2000},
+        J2000d: {label: "J2000d", system: systems.J2000},
+        GAL:  {label: "Galactic", system: systems.GAL}
+    };
+
+})();
+
+
+
+CooFrameEnum.fromString = function(str, defaultValue) {
+    if (! str) {
+        return defaultValue ? defaultValue : null;
+    }
+
+    str = str.toLowerCase().replace(/^\s+|\s+$/g, ''); // convert to lowercase and trim
+
+    if (str.indexOf('j2000d')==0 || str.indexOf('icrsd')==0) {
+        return CooFrameEnum.J2000d;
+    }
+    else if (str.indexOf('j2000')==0 || str.indexOf('icrs')==0) {
+        return CooFrameEnum.J2000;
+    }
+    else if (str.indexOf('gal')==0) {
+        return CooFrameEnum.GAL;
+    }
+    else {
+        return defaultValue ? defaultValue : null;
+    }
+};
+
+// Copyright 2013-2017 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File HiPSDefinition
+ *
+ * Author: Thomas Boch [CDS]
+ *
+ *****************************************************************************/
+HiPSDefinition = (function() {
+
+    // constructor
+    var HiPSDefinition = function(properties) {
+        this.properties = properties; // key-value object corresponding to the properties file
+
+        this.id = this.getID();
+        this.obsTitle = properties['obs_title'];
+        this.frame = properties['hips_frame'];
+        this.order = parseInt(properties['hips_order']);
+        this.clientSortKey = properties['client_sort_key'];
+        this.tileFormats = properties.hasOwnProperty('hips_tile_format') && properties['hips_tile_format'].split(' ');
+        this.urls = [];
+        this.urls.push(properties['hips_service_url']);
+        var k = 1;
+        while (properties.hasOwnProperty('hips_service_url_' + k)) {
+            this.urls.push(properties['hips_service_url_' + k]);
+            k++;
+        }
+
+        this.clientApplications = properties['client_application'];
+    };
+
+    HiPSDefinition.prototype = {
+
+        getServiceURLs: function(httpsOnly) {
+            httpsOnly = httpsOnly === true;
+
+            // TODO: TO BE COMPLETED
+        },
+
+        // return the ID according to the properties
+        getID: function() {
+            // ID is explicitely given
+            if (this.properties.hasOwnProperty('ID')) {
+                return this.properties['ID'];
+            }
+
+            var id = null;
+            // ID might be built from different fields
+            if (this.properties.hasOwnProperty('creator_did')) {
+                id = this.properties['creator_did'];
+            }
+            if (id==null && this.properties.hasOwnProperty('publisher_did')) {
+                id = this.properties['publisher_did'];
+            }
+
+            if (id != null) {
+                // remove ivo:// prefix
+                if (id.slice(0, 6) === 'ivo://') {
+                    id = id.slice(6);
+                }
+
+                // '?' are replaced by '/'
+                id = id.replace(/\?/g, '/')
+            }
+
+            return id;
+        }
+
+
+
+    };
+
+    // cache (at the source code level) of the list of HiPS
+    // this is the result to a query to http://alasky.u-strasbg.fr/MocServer/query?dataproduct_type=image&client_application=AladinLite&fmt=json&fields=ID,obs_title,client_sort_key,client_application,hips_service_url*,hips_order,hips_tile_format,hips_frame
+    var AL_CACHE_CLASS_LEVEL = [{
+    "ID": "CDS/P/2MASS/color",
+    "obs_title": "2MASS color J (1.23 microns), H (1.66 microns), K (2.16 microns)",
+    "client_sort_key": "04-001-00",
+    "client_application":[ "AladinLite", "AladinDesktop"],
+    "hips_order": "9",
+    "hips_frame": "equatorial",
+    "hips_tile_format": "jpeg",
+    "hips_service_url": "http://alasky.unistra.fr/2MASS/Color",
+    "hips_service_url_1": "http://alaskybis.unistra.fr/2MASS/Color",
+    "hips_service_url_2": "https://alaskybis.unistra.fr/2MASS/Color"
+    }, {
+    "ID": "CDS/P/AKARI/FIS/Color",
+    "obs_title": "AKARI Far-infrared All-Sky Survey - color composition WideL/WideS/N60",
+    "client_sort_key": "04-05-00",
+    "client_application":[ "AladinLite", "AladinDesktop"],
+    "hips_order": "5",
+    "hips_frame": "equatorial",
+    "hips_tile_format": "png jpeg",
+    "hips_service_url": "http://alasky.unistra.fr/AKARI-FIS/ColorLSN60",
+    "hips_service_url_1": "http://alaskybis.unistra.fr/AKARI-FIS/ColorLSN60",
+    "hips_service_url_2": "https://alaskybis.unistra.fr/AKARI-FIS/ColorLSN60"
+    }, {
+    "ID": "CDS/P/DECaLS/DR3/color",
+    "obs_title": "DECaLS DR3 color",
+    "hips_frame": "equatorial",
+    "hips_order": "11",
+    "hips_tile_format": "jpeg",
+    "hips_service_url": "http://alasky.unistra.fr/DECaLS/DR3/color"
+}, {
+    "ID": "CDS/P/DSS2/blue",
+    "obs_title": "DSS2 Blue (XJ+S)",
+    "client_sort_key": "03-01-03",
+    "client_application":[ "AladinLite", "AladinDesktop"],
+    "hips_order": "9",
+    "hips_frame": "equatorial",
+    "hips_tile_format": "jpeg fits",
+    "hips_service_url": "http://alasky.unistra.fr/DSS/DSS2-blue-XJ-S",
+    "hips_service_url_1": "http://alaskybis.unistra.fr/DSS/DSS2-blue-XJ-S",
+    "hips_service_url_2": "https://alaskybis.unistra.fr/DSS/DSS2-blue-XJ-S",
+    "hips_service_url_3": "http://healpix.ias.u-psud.fr/DSS2Blue"
+}, {
+    "ID": "CDS/P/DSS2/color",
+    "obs_title": "DSS colored",
+    "client_sort_key": "03-00",
+    "client_application":[ "AladinLite", "AladinDesktop"],
+    "hips_order": "9",
+    "hips_frame": "equatorial",
+    "hips_tile_format": "jpeg",
+    "hips_service_url": "http://alasky.unistra.fr/DSS/DSSColor",
+    "hips_service_url_1": "http://alaskybis.unistra.fr/DSS/DSSColor",
+    "hips_service_url_2": "https://alaskybis.unistra.fr/DSS/DSSColor",
+    "hips_service_url_3": "http://healpix.ias.u-psud.fr/DSSColorNew",
+    "hips_service_url_4": "http://skies.esac.esa.int/DSSColor/"
+}, {
+    "ID": "CDS/P/DSS2/red",
+    "obs_title": "DSS2 Red (F+R)",
+    "client_sort_key": "03-01-02",
+    "client_application":[ "AladinLite", "AladinDesktop"],
+    "hips_order": "9",
+    "hips_frame": "equatorial",
+    "hips_tile_format": "jpeg fits",
+    "hips_service_url": "http://alasky.unistra.fr/DSS/DSS2Merged",
+    "hips_service_url_1": "http://alaskybis.unistra.fr/DSS/DSS2Merged",
+    "hips_service_url_2": "https://alaskybis.unistra.fr/DSS/DSS2Merged",
+    "hips_service_url_3": "http://healpix.ias.u-psud.fr/DSS2Merged"
+}, {
+    "ID": "P/PanSTARRS/DR1/g",
+    "hips_service_url": "http://alasky.u-strasbg.fr/Pan-STARRS/DR1/g",
+    "obs_title": "PanSTARRS DR1 g",
+    "hips_order": 11,
+    "hips_frame": "equatorial",
+    "hips_tile_format": "jpeg fits"
+}, {
+    "ID": "CDS/P/Fermi/color",
+    "obs_title": "Fermi Color HEALPix survey",
+    "client_sort_key": "00-01-01",
+    "client_application":[ "AladinLite", "AladinDesktop"],
+    "hips_order": "3",
+    "hips_frame": "equatorial",
+    "hips_tile_format": "jpeg",
+    "hips_service_url": "http://alasky.unistra.fr/Fermi/Color",
+    "hips_service_url_1": "http://alaskybis.unistra.fr/Fermi/Color",
+    "hips_service_url_2": "https://alaskybis.unistra.fr/Fermi/Color"
+}, {
+    "ID": "CDS/P/Finkbeiner",
+    "obs_title": "Finkbeiner Halpha composite survey",
+    "client_sort_key": "06-01",
+    "client_application":[ "AladinLite", "AladinDesktop"],
+    "hips_order": "3",
+    "hips_frame": "galactic",
+    "hips_tile_format": "jpeg fits",
+    "hips_service_url": "http://alasky.unistra.fr/FinkbeinerHalpha",
+    "hips_service_url_1": "http://alaskybis.unistra.fr/FinkbeinerHalpha",
+    "hips_service_url_2": "https://alaskybis.unistra.fr/FinkbeinerHalpha"
+}, {
+    "ID": "CDS/P/GALEXGR6/AIS/color",
+    "obs_title": "GALEX GR6 AIS (until March 2014)- Color composition",
+    "client_sort_key": "02-01-01",
+    "client_application":[ "AladinLite", "AladinDesktop"],
+    "hips_order": "8",
+    "hips_frame": "equatorial",
+    "hips_tile_format": "png jpeg",
+    "hips_service_url": "http://alasky.unistra.fr/GALEX/GR6-03-2014/AIS-Color",
+    "hips_service_url_1": "http://alaskybis.unistra.fr/GALEX/GR6-03-2014/AIS-Color",
+    "hips_service_url_2": "https://alaskybis.unistra.fr/GALEX/GR6-03-2014/AIS-Color"
+}, {
+    "ID": "CDS/P/IRIS/color",
+    "obs_title": "IRAS-IRIS HEALPix survey, color",
+    "client_sort_key": "04-02-01",
+    "client_application":[ "AladinLite", "AladinDesktop"],
+    "hips_order": "3",
+    "hips_frame": "galactic",
+    "hips_tile_format": "jpeg",
+    "hips_service_url": "http://alasky.unistra.fr/IRISColor",
+    "hips_service_url_1": "http://alaskybis.unistra.fr/IRISColor",
+    "hips_service_url_2": "https://alaskybis.unistra.fr/IRISColor",
+    "hips_service_url_3": "http://healpix.ias.u-psud.fr/IRISColor",
+    "hips_service_url_4": "http://skies.esac.esa.int/IRISColor/"
+}, {
+    "ID": "CDS/P/Mellinger/color",
+    "obs_title": "Mellinger optical survey, color",
+    "client_sort_key": "03-03",
+    "client_application":[ "AladinLite", "AladinDesktop"],
+    "hips_order": "4",
+    "hips_frame": "galactic",
+    "hips_tile_format": "jpeg",
+    "hips_service_url": "http://alasky.unistra.fr/MellingerRGB",
+    "hips_service_url_1": "http://alaskybis.unistra.fr/MellingerRGB",
+    "hips_service_url_2": "https://alaskybis.unistra.fr/MellingerRGB"
+}, {
+    "ID": "CDS/P/SDSS9/color",
+    "obs_title": "SDSS 9 color",
+    "client_sort_key": "03-02-01",
+    "client_application":[ "AladinLite", "AladinDesktop"],
+    "hips_order": "10",
+    "hips_frame": "equatorial",
+    "hips_tile_format": "jpeg",
+    "hips_service_url": "http://alasky.unistra.fr/SDSS/DR9/color",
+    "hips_service_url_1": "http://alaskybis.unistra.fr/SDSS/DR9/color",
+    "hips_service_url_2": "https://alaskybis.unistra.fr/SDSS/DR9/color",
+    "hips_service_url_3": "http://healpix.ias.u-psud.fr/SDSS9Color",
+    "hips_service_url_4": "http://skies.esac.esa.int/SDSS9Color/"
+}, {
+    "ID": "CDS/P/SPITZER/color",
+    "obs_title": "IRAC HEALPix survey, color",
+    "client_sort_key": "04-03-00",
+    "client_application":[ "AladinLite", "AladinDesktop"],
+    "hips_order": "9",
+    "hips_frame": "galactic",
+    "hips_tile_format": "jpeg",
+    "hips_service_url": "http://alasky.unistra.fr/SpitzerI1I2I4color",
+    "hips_service_url_1": "http://alaskybis.unistra.fr/SpitzerI1I2I4color",
+    "hips_service_url_2": "https://alaskybis.unistra.fr/SpitzerI1I2I4color",
+    "hips_service_url_3": "http://healpix.ias.u-psud.fr/SPITZERColor"
+}, {
+    "ID": "CDS/P/allWISE/color",
+    "obs_title": "AllWISE color  Red (W4) , Green (W2) , Blue (W1) from raw Atlas Images",
+    "client_sort_key": "04-003-00",
+    "client_application":[ "AladinLite", "AladinDesktop"],
+    "hips_order": "8",
+    "hips_frame": "equatorial",
+    "hips_tile_format": "jpeg",
+    "hips_service_url": "http://alasky.unistra.fr/AllWISE/RGB-W4-W2-W1",
+    "hips_service_url_1": "http://alaskybis.unistra.fr/AllWISE/RGB-W4-W2-W1",
+    "hips_service_url_2": "https://alaskybis.unistra.fr/AllWISE/RGB-W4-W2-W1"
+}, {
+    "ID": "IPAC/P/GLIMPSE360",
+    "obs_title": "GLIMPSE360: Spitzer's Infrared Milky Way",
+    "client_sort_key": "04-03-0",
+    "client_application":[ "AladinLite", "AladinDesktop"],
+    "hips_order": "9",
+    "hips_frame": "equatorial",
+    "hips_tile_format": "jpeg",
+    "hips_service_url": "http://www.spitzer.caltech.edu/glimpse360/aladin/data"
+}, {
+    "ID": "JAXA/P/MAXI_SSC_SUM",
+    "hips_tile_format": "png",
+    "hips_frame": "equatorial",
+    "obs_title": "MAXI SSC all-sky image integrated for 4.5 years",
+    "hips_order": "6",
+    "hips_service_url": "http://darts.isas.jaxa.jp/pub/judo2/HiPS/maxi_ssc_sum",
+    "hips_service_url_1": "http://alasky.unistra.fr//JAXA/JAXA_P_MAXI_SSC_SUM",
+    "hips_service_url_2": "http://alaskybis.unistra.fr//JAXA/JAXA_P_MAXI_SSC_SUM",
+    "hips_service_url_3": "https://alaskybis.unistra.fr//JAXA/JAXA_P_MAXI_SSC_SUM"
+}, {
+    "ID": "JAXA/P/SWIFT_BAT_FLUX",
+    "hips_tile_format": "png",
+    "hips_frame": "equatorial",
+    "obs_title": "Swift-BAT 70-month all-sray hard X-ray survey image",
+    "hips_order": "6",
+    "hips_service_url": "http://darts.isas.jaxa.jp/pub/judo2/HiPS/swift_bat_flux/",
+    "hips_service_url_1": "http://alasky.unistra.fr//JAXA/JAXA_P_SWIFT_BAT_FLUX",
+    "hips_service_url_2": "http://alaskybis.unistra.fr//JAXA/JAXA_P_SWIFT_BAT_FLUX",
+    "hips_service_url_3": "https://alaskybis.unistra.fr//JAXA/JAXA_P_SWIFT_BAT_FLUX"
+}, {
+    "ID": "ov-gso/P/VTSS/Ha",
+    "obs_title": "Virginia Tech Spectral-Line Survey (VTSS) - Halpha image",
+    "client_sort_key": "06-xx",
+    "client_application":[ "AladinLite", "AladinDesktop"],
+    "hips_order": "3",
+    "hips_frame": ["galactic", "galactic"],
+    "hips_tile_format": "png jpeg fits",
+    "hips_service_url": "http://cade.irap.omp.eu/documents/Ancillary/4Aladin/VTSS",
+    "hips_service_url_1": "http://alasky.unistra.fr/IRAP/VTSS",
+    "hips_service_url_2": "http://alaskybis.unistra.fr/IRAP/VTSS",
+    "hips_service_url_3": "https://alaskybis.unistra.fr/IRAP/VTSS"
+}, {
+    "ID": "xcatdb/P/XMM/EPIC",
+    "obs_title": "XMM-Newton stacked EPIC images",
+    "hips_frame": "equatorial",
+    "hips_order": "7",
+    "hips_service_url": "http://saada.u-strasbg.fr/xmmallsky",
+    "hips_tile_format": "png fits",
+    "hips_service_url_1": "http://alasky.unistra.fr/SSC/xmmallsky",
+    "hips_service_url_2": "http://alaskybis.unistra.fr/SSC/xmmallsky",
+    "hips_service_url_3": "https://alaskybis.unistra.fr/SSC/xmmallsky"
+}, {
+    "ID": "xcatdb/P/XMM/PN/color",
+    "obs_title": "False color X-ray images (Red=0.5-1 Green=1-2 Blue=2-4.5)Kev",
+    "hips_order": "7",
+    "hips_frame": "equatorial",
+    "hips_tile_format": "png jpeg",
+    "hips_service_url": "http://saada.unistra.fr/PNColor",
+    "hips_service_url_1": "http://alasky.u-strasbg.fr/SSC/xcatdb_P_XMM_PN_color",
+    "hips_service_url_2": "http://alaskybis.u-strasbg.fr/SSC/xcatdb_P_XMM_PN_color",
+    "hips_service_url_3": "https://alaskybis.u-strasbg.fr/SSC/xcatdb_P_XMM_PN_color"
+}];
+
+    var listHipsProperties = []; // this variable stores our current knowledge
+
+    HiPSDefinition.LOCAL_STORAGE_KEY = 'aladin:hips-list';
+
+    var RETRIEVAL_TIMESTAMP_KEY = '_timestamp_retrieved';
+    var LAST_URL_KEY = '_last_used_url'; // URL previousy used to retrieve data from this HiPS
+    // retrieve definitions previousy stored in local storage
+    // @return an array with the HiPS definitions, empty array if nothing found or if an error occured
+    HiPSDefinition.getLocalStorageDefinitions = function() {
+        try {
+            var defs = window.localStorage.getItem(HiPSDefinition.LOCAL_STORAGE_KEY);
+            return defs === null ? [] : window.JSON.parse(defs);
+        }
+        catch(e) {
+            // silently fail and return empty array
+            return [];
+        }
+    };
+
+    // store in local storage a list of HiPSDefinition objects
+    // @return true if storage was successful
+    HiPSDefinition.storeInLocalStorage = function(properties) {
+        try {
+            window.localStorage.setItem(HiPSDefinition.LOCAL_STORAGE_KEY, window.JSON.stringify(properties));
+        }
+        catch(e) {
+            // silently fail and return false
+            return false;
+        }
+
+        return true;
+    };
+
+    var MOCSERVER_MIRRORS_HTTP = ['http://alasky.u-strasbg.fr/MocServer/query', 'http://alaskybis.u-strasbg.fr/MocServer/query']; // list of base URL for MocServer mirrors, available in HTTP
+    var MOCSERVER_MIRRORS_HTTPS = ['https://alasky.u-strasbg.fr/MocServer/query', 'https://alaskybis.unistra.fr/MocServer/query']; // list of base URL for MocServer mirrors, available in HTTPS
+
+    // get HiPS definitions, by querying the MocServer
+    // return data as dict-like objects
+    HiPSDefinition.getRemoteDefinitions = function(params, successCallbackFn, failureCallbackFn) {
+        var params = params || {client_application: 'AladinLite'}; // by default, retrieve only HiPS tagged "Aladin Lite"
+
+        params['fmt'] = 'json';
+        params['fields'] = 'ID,obs_title,client_sort_key,client_application,hips_service_url*,hips_order,hips_tile_format,hips_frame';
+
+        var urls = Utils.isHttpsContext() ? MOCSERVER_MIRRORS_HTTPS : MOCSERVER_MIRRORS_HTTP;
+
+        var successCallback = function(data) {
+            (typeof successCallbackFn === 'function') && successCallbackFn(data);
+        };
+        var failureCallback = function() {
+            console.error('Could not load HiPS definitions from urls ' + urls);
+            (typeof failureCallbackFn === 'function') && failureCallbackFn();
+        };
+
+        Utils.loadFromMirrors(urls, {data: params, onSuccess: successCallback, onFailure: failureCallback, timeout: 5});
+    };
+
+    // complement the baseList with the items in newList
+    var merge = function(baseList, newList) {
+        var updatedList = [];
+        var newListById = {};
+        for (var k=0; k<newList.length; k++) {
+            var item = newList[k];
+            newListById[item.ID] = item;
+        }
+
+        for (var k=0; k<baseList.length; k++) {
+            var item = baseList[k];
+            var id = item.ID;
+            if (newListById.hasOwnProperty(id)) {
+                var itemToAdd = newListById[id];
+                // we keep the last used URL property
+                if (item.hasOwnProperty(LAST_URL_KEY) && ! itemToAdd.hasOwnProperty(LAST_URL_KEY)) {
+                    itemToAdd[LAST_URL_KEY] = item[LAST_URL_KEY];
+                }
+                updatedList.push(itemToAdd);
+            }
+            else {
+                updatedList.push(item);
+            }
+        }
+
+        return updatedList;
+    };
+
+    HiPSDefinition.CACHE_RETENTION_TIME_SECONDS = 7 * 86400; // definitions can be kept 7 days
+    HiPSDefinition.init = function() {
+        // first, merge local definitions at class level with definitions in local storage
+        listHipsProperties = AL_CACHE_CLASS_LEVEL;
+
+        // second, remove old definitions (client != AladinLite and timestamp older than CACHE_RETENTION_TIME_SECONDS) and merge
+        var localDefs = HiPSDefinition.getLocalStorageDefinitions();
+        // 2.1 remove old defs
+        var now = new Date().getTime();
+        var indicesToRemove = [];
+        for (var k=0; k<localDefs.length; k++) {
+            var def = localDefs[k];
+            if (def.hasOwnProperty(RETRIEVAL_TIMESTAMP_KEY) && (now - def[RETRIEVAL_TIMESTAMP_KEY]) > 1000 * HiPSDefinition.CACHE_RETENTION_TIME_SECONDS) {
+                indicesToRemove.push(k);
+            }
+        }
+        // we have to browse the array in reverse order in order not to mess up indices
+        for (var k = indicesToRemove.length - 1; k >= 0; k--) {
+            localDefs.splice(indicesToRemove[k],1);
+        }
+        // 2.2 merge
+        listHipsProperties = merge(listHipsProperties, localDefs);
+
+        // third, retrieve remote definitions, merge and save
+        HiPSDefinition.getRemoteDefinitions({dataproduct_type: 'image', client_application: 'AladinLite'}, function(remoteDefs) {
+            // adding timestamp of retrieval
+            var now = new Date().getTime();
+            for (var k=0; k<remoteDefs.length; k++) {
+                remoteDefs[k][RETRIEVAL_TIMESTAMP_KEY] = now;
+            }
+            listHipsProperties = merge(listHipsProperties, remoteDefs);
+            HiPSDefinition.storeInLocalStorage(listHipsProperties);
+        });
+
+    };
+
+    // return list of HiPSDefinition objects, filtering out definitions whose client_application is not AladinLite
+    HiPSDefinition.getALDefaultHiPSDefinitions = function() {
+        // filter out definitions with client_application != 'AladinLite'
+        var ret = [];
+        for (var k=0; k<listHipsProperties.length; k++) {
+            var properties = listHipsProperties[k];
+            if ( ! properties.hasOwnProperty('client_application') || properties['client_application'].indexOf('AladinLite')<0) {
+                continue;
+            }
+
+            ret.push(new HiPSDefinition(properties));
+        }
+
+        return ret;
+    };
+
+    // return list of known HiPSDefinition objects
+    HiPSDefinition.getDefinitions = function() {
+        var ret = [];
+        for (var k=0; k<listHipsProperties.length; k++) {
+            var properties = listHipsProperties[k];
+            ret.push(new HiPSDefinition(properties));
+        }
+
+        return ret;
+    };
+
+    // parse a HiPS properties and return a dict-like object with corresponding key-values
+    // return null if parsing failed
+    HiPSDefinition.parseHiPSProperties = function(propertiesStr) {
+        if (propertiesStr==null) {
+            return null;
+        }
+
+        var propertiesDict = {};
+        // remove CR characters
+        propertiesStr = propertiesStr.replace(/[\r]/g, '');
+        // split on LF
+        var lines = propertiesStr.split('\n');
+        for (var k=0; k<lines.length; k++)  {
+            var l = $.trim(lines[k]);
+            // ignore comments lines
+            if (l.slice(0, 1)==='#') {
+                continue;
+            }
+            var idx = l.indexOf('=');
+            if (idx<0) {
+                continue;
+            }
+            var key = $.trim(l.slice(0, idx));
+            var value = $.trim(l.slice(idx+1));
+
+            propertiesDict[key] = value;
+        }
+
+        return propertiesDict;
+    };
+
+
+    // find a HiPSDefinition by id.
+    // look first locally, and remotely only if local search was unsuccessful
+    //
+    // call callback function with a list of HiPSDefinition candidates, empty array if nothing found
+
+    HiPSDefinition.findByID = function(id, callback) {
+        // look first locally
+        var candidates = findByIDLocal(id);
+        if (candidates.length>0) {
+            (typeof callback === 'function') && callback(candidates);
+            return;
+        }
+
+        // then remotely
+        findByIDRemote(id, callback);
+    };
+
+    // find a HiPSDefinition by id.
+    // search is done on the local knowledge of HiPSDefinitions
+    HiPSDefinition.findByIDLocal = function(id2search, callback) {
+        var candidates = [];
+        for (var k=0; k<listHipsProperties.length; k++) {
+            var properties = listHipsProperties[k];
+            var id = properties['ID'];
+            if (id.match(id2search) != null ) {
+                candidates.push(new HiPSDefinition(properties));
+            }
+        }
+
+        return candidates;
+    };
+
+    // find remotely a HiPSDefinition by ID
+    HiPSDefinition.findByIDRemote = function(id, callback) {
+        HiPSDefinition.findHiPSRemote({ID: '*' + id + '*'}, callback);
+    };
+
+    // search a HiPS according to some criteria
+    HiPSDefinition.findHiPSRemote = function(searchOptions, callback) {
+        searchOptions = searchOptions || {};
+        if (! searchOptions.hasOwnProperty('dataproduct_type')) {
+            searchOptions['dataproduct_type'] = 'image';
+        }
+        HiPSDefinition.getRemoteDefinitions(searchOptions, function(candidates) {
+            var defs = [];
+            for (var k=0; k<candidates.length; k++) {
+                defs.push(new HiPSDefinition(candidates[k]));
+            }
+            (typeof callback === 'function') && callback(defs);
+        });
+    };
+
+
+    // Create a HiPSDefinition object from a URL
+    //
+    // If the URL ends with 'properties', it is assumed to be the URL of the properties file
+    // else, it is assumed to be the base URL of the HiPS
+    //
+    // return a HiPSDefinition if successful, null if it failed
+    HiPSDefinition.fromURL = function(url, callback) {
+        var hipsUrl, propertiesUrl;
+        if (url.slice(-10) === 'properties') {
+            propertiesUrl = url;
+            hipsUrl = propertiesUrl.slice(0, -11);
+        }
+        else {
+            if (url.slice(-1) === '/') {
+                url = url.slice(0, -1);
+            }
+            hipsUrl = url;
+            propertiesUrl = hipsUrl + '/properties';
+        }
+
+        var callbackWhenPropertiesLoaded = function(properties) {
+            // Sometimes, hips_service_url is missing. That can happen for instance Hipsgen does not set the hips_service_url keyword
+            // --> in that case, we add as an attribyte the URL that was given as input parameter
+            var hipsPropertiesDict = HiPSDefinition.parseHiPSProperties(properties);
+            if (! hipsPropertiesDict.hasOwnProperty('hips_service_url')) {
+                hipsPropertiesDict['hips_service_url'] = hipsUrl;
+            }
+            (typeof callback === 'function') && callback(new HiPSDefinition(hipsPropertiesDict));
+        };
+
+        // try first without proxy
+        var ajax = Utils.getAjaxObject(propertiesUrl, 'GET', 'text', false);
+        ajax
+            .done(function(data) {
+                callbackWhenPropertiesLoaded(data);
+            })
+            .fail(function() {
+                // if not working, try with the proxy
+                var ajax = Utils.getAjaxObject(propertiesUrl, 'GET', 'text', true);
+                ajax
+                    .done(function(data) {
+                        callbackWhenPropertiesLoaded(data);
+                    })
+                    .fail(function() {
+                        (typeof callback === 'function') && callback(null);
+                    })
+            });
+    };
+
+    // HiPSDefinition generation from a properties dict-like object
+    HiPSDefinition.fromProperties = function(properties) {
+        return new HiPSDefinition(properties);
+    };
+
+
+
+
+    HiPSDefinition.init();
+
+    return HiPSDefinition;
+
+})();
+
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File Downloader
+ * Queue downloading for image elements
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+Downloader = (function() {
+	var NB_MAX_SIMULTANEOUS_DL = 4;
+	// TODO : le fading ne marche pas bien actuellement
+	var FADING_ENABLED = false;
+	var FADING_DURATION = 700; // in milliseconds
+
+
+	var Downloader = function(view) {
+		this.view = view; // reference to the view to be able to request redraw
+		this.nbDownloads = 0; // number of current downloads
+		this.dlQueue = []; // queue of items being downloaded
+        this.urlsInQueue = {};
+	};
+
+	Downloader.prototype.emptyQueue = function() {
+		this.dlQueue = [];
+        this.urlsInQueue = {};
+    };
+
+	Downloader.prototype.requestDownload = function(img, url, cors) {
+        // first check if url already in queue
+        if (url in this.urlsInQueue)  {
+            return;
+        }
+		// put in queue
+		this.dlQueue.push({img: img, url: url, cors: cors});
+		this.urlsInQueue[url] = 1;
+
+		this.tryDownload();
+	};
+
+	// try to download next items in queue if possible
+	Downloader.prototype.tryDownload = function() {
+	    //if (this.dlQueue.length>0 && this.nbDownloads<NB_MAX_SIMULTANEOUS_DL) {
+		while (this.dlQueue.length>0 && this.nbDownloads<NB_MAX_SIMULTANEOUS_DL) {
+			this.startDownloadNext();
+		}
+	};
+
+	Downloader.prototype.startDownloadNext = function() {
+		// get next in queue
+		var next = this.dlQueue.shift();
+		if ( ! next) {
+			return;
+		}
+
+		this.nbDownloads++;
+		var downloaderRef = this;
+		next.img.onload = function() {
+			downloaderRef.completeDownload(this, true); // in this context, 'this' is the Image
+		};
+
+		next.img.onerror = function(e) {
+			downloaderRef.completeDownload(this, false); // in this context, 'this' is the Image
+		};
+		if (next.cors) {
+		    next.img.crossOrigin = 'anonymous';
+		}
+
+		else {
+		    if (next.img.crossOrigin !== undefined) {
+		        delete next.img.crossOrigin;
+		    }
+		}
+
+
+		next.img.src = next.url;
+	};
+
+	Downloader.prototype.completeDownload = function(img, success) {
+        delete this.urlsInQueue[img.src];
+		img.onerror = null;
+		img.onload = null;
+		this.nbDownloads--;
+		if (success) {
+			if (FADING_ENABLED) {
+				var now = new Date().getTime();
+				img.fadingStart = now;
+				img.fadingEnd = now + FADING_DURATION;
+			}
+			this.view.requestRedraw();
+		}
+		else {
+		    img.dlError = true;
+		}
+
+		this.tryDownload();
+	};
+
+
+
+	return Downloader;
+})();
+// Generated by CoffeeScript 1.6.3
+(function() {
+  var Base, BinaryTable, CompressedImage, DataUnit, Decompress, FITS, HDU, Header, HeaderVerify, Image, ImageUtils, Parser, Table, Tabular, _ref, _ref1,
+    __hasProp = {}.hasOwnProperty,
+    __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+    __slice = [].slice;
+
+  if (this.astro == null) {
+    this.astro = {};
+  }
+
+  Base = (function() {
+    function Base() {}
+
+    Base.include = function(obj) {
+      var key, value;
+      for (key in obj) {
+        value = obj[key];
+        this.prototype[key] = value;
+      }
+      return this;
+    };
+
+    Base.extend = function(obj) {
+      var key, value;
+      for (key in obj) {
+        value = obj[key];
+        this[key] = value;
+      }
+      return this;
+    };
+
+    Base.prototype.proxy = function(func) {
+      var _this = this;
+      return function() {
+        return func.apply(_this, arguments);
+      };
+    };
+
+    Base.prototype.invoke = function(callback, opts, data) {
+      var context;
+      context = (opts != null ? opts.context : void 0) != null ? opts.context : this;
+      if (callback != null) {
+        return callback.call(context, data, opts);
+      }
+    };
+
+    return Base;
+
+  })();
+
+  Parser = (function(_super) {
+    __extends(Parser, _super);
+
+    Parser.prototype.LINEWIDTH = 80;
+
+    Parser.prototype.BLOCKLENGTH = 2880;
+
+    File.prototype.slice = File.prototype.slice || File.prototype.webkitSlice;
+
+    Blob.prototype.slice = Blob.prototype.slice || Blob.prototype.webkitSlice;
+
+    function Parser(arg, callback, opts) {
+      var xhr,
+        _this = this;
+      this.arg = arg;
+      this.callback = callback;
+      this.opts = opts;
+      this.hdus = [];
+      this.blockCount = 0;
+      this.begin = 0;
+      this.end = this.BLOCKLENGTH;
+      this.offset = 0;
+      this.headerStorage = new Uint8Array();
+      if (typeof this.arg === 'string') {
+        this.readNextBlock = this._readBlockFromBuffer;
+        xhr = new XMLHttpRequest();
+        xhr.open('GET', this.arg);
+        xhr.responseType = 'arraybuffer';
+
+        // the onerror handling has been added wrt the original fitsjs library as retrieved on the astrojs github repo
+        // if an error occurs, we return an empty object
+        xhr.onerror = function() {
+          _this.invoke(_this.callback, _this.opts);
+        }
+
+        xhr.onload = function() {
+          if (xhr.status !== 200) {
+            _this.invoke(_this.callback, _this.opts);
+            return;
+          }
+          _this.arg = xhr.response;
+          _this.length = _this.arg.byteLength;
+          return _this.readFromBuffer();
+        };
+        xhr.send();
+      } else {
+        this.length = this.arg.size;
+        this.readNextBlock = this._readBlockFromFile;
+        this.readFromFile();
+      }
+    }
+
+    Parser.prototype.readFromBuffer = function() {
+      var block;
+      block = this.arg.slice(this.begin + this.offset, this.end + this.offset);
+      return this.readBlock(block);
+    };
+
+    Parser.prototype.readFromFile = function() {
+      var block,
+        _this = this;
+      this.reader = new FileReader();
+      this.reader.onloadend = function(e) {
+        return _this.readBlock(e.target.result);
+      };
+      block = this.arg.slice(this.begin + this.offset, this.end + this.offset);
+      return this.reader.readAsArrayBuffer(block);
+    };
+
+    Parser.prototype.readBlock = function(block) {
+      var arr, dataLength, dataunit, header, rowIndex, rows, s, slice, tmp, value, _i, _len, _ref;
+      arr = new Uint8Array(block);
+      tmp = new Uint8Array(this.headerStorage);
+      this.headerStorage = new Uint8Array(this.end);
+      this.headerStorage.set(tmp, 0);
+      this.headerStorage.set(arr, this.begin);
+      rows = this.BLOCKLENGTH / this.LINEWIDTH;
+      while (rows--) {
+        rowIndex = rows * this.LINEWIDTH;
+        if (arr[rowIndex] === 32) {
+          continue;
+        }
+        if (arr[rowIndex] === 69 && arr[rowIndex + 1] === 78 && arr[rowIndex + 2] === 68 && arr[rowIndex + 3] === 32) {
+          s = '';
+          _ref = this.headerStorage;
+          for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+            value = _ref[_i];
+            s += String.fromCharCode(value);
+          }
+          header = new Header(s);
+          this.start = this.end + this.offset;
+          dataLength = header.getDataLength();
+          slice = this.arg.slice(this.start, this.start + dataLength);
+          if (header.hasDataUnit()) {
+            dataunit = this.createDataUnit(header, slice);
+          }
+          this.hdus.push(new HDU(header, dataunit));
+          this.offset += this.end + dataLength + this.excessBytes(dataLength);
+          if (this.offset === this.length) {
+            this.headerStorage = null;
+            this.invoke(this.callback, this.opts, this);
+            return;
+          }
+          this.blockCount = 0;
+          this.begin = this.blockCount * this.BLOCKLENGTH;
+          this.end = this.begin + this.BLOCKLENGTH;
+          this.headerStorage = new Uint8Array();
+          block = this.arg.slice(this.begin + this.offset, this.end + this.offset);
+          this.readNextBlock(block);
+          return;
+        }
+        break;
+      }
+      this.blockCount += 1;
+      this.begin = this.blockCount * this.BLOCKLENGTH;
+      this.end = this.begin + this.BLOCKLENGTH;
+      block = this.arg.slice(this.begin + this.offset, this.end + this.offset);
+      this.readNextBlock(block);
+    };
+
+    Parser.prototype._readBlockFromBuffer = function(block) {
+      return this.readBlock(block);
+    };
+
+    Parser.prototype._readBlockFromFile = function(block) {
+      return this.reader.readAsArrayBuffer(block);
+    };
+
+    Parser.prototype.createDataUnit = function(header, blob) {
+      var type;
+      type = header.getDataType();
+      return new astro.FITS[type](header, blob);
+    };
+
+    Parser.prototype.excessBytes = function(length) {
+      return (this.BLOCKLENGTH - (length % this.BLOCKLENGTH)) % this.BLOCKLENGTH;
+    };
+
+    Parser.prototype.isEOF = function() {
+      if (this.offset === this.length) {
+        return true;
+      } else {
+        return false;
+      }
+    };
+
+    return Parser;
+
+  })(Base);
+
+  FITS = (function(_super) {
+    __extends(FITS, _super);
+
+    function FITS(arg, callback, opts) {
+      var parser,
+        _this = this;
+      this.arg = arg;
+      parser = new Parser(this.arg, function(fits) {
+        _this.hdus = parser.hdus;
+        return _this.invoke(callback, opts, _this);
+      });
+    }
+
+    FITS.prototype.getHDU = function(index) {
+      var hdu, _i, _len, _ref;
+      if ((index != null) && (this.hdus[index] != null)) {
+        return this.hdus[index];
+      }
+      _ref = this.hdus;
+      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+        hdu = _ref[_i];
+        if (hdu.hasData()) {
+          return hdu;
+        }
+      }
+    };
+
+    FITS.prototype.getHeader = function(index) {
+      return this.getHDU(index).header;
+    };
+
+    FITS.prototype.getDataUnit = function(index) {
+      return this.getHDU(index).data;
+    };
+
+    return FITS;
+
+  })(Base);
+
+  FITS.version = '0.6.5';
+
+  this.astro.FITS = FITS;
+
+  DataUnit = (function(_super) {
+    __extends(DataUnit, _super);
+
+    DataUnit.swapEndian = {
+      B: function(value) {
+        return value;
+      },
+      I: function(value) {
+        return (value << 8) | (value >> 8);
+      },
+      J: function(value) {
+        return ((value & 0xFF) << 24) | ((value & 0xFF00) << 8) | ((value >> 8) & 0xFF00) | ((value >> 24) & 0xFF);
+      }
+    };
+
+    DataUnit.swapEndian[8] = DataUnit.swapEndian['B'];
+
+    DataUnit.swapEndian[16] = DataUnit.swapEndian['I'];
+
+    DataUnit.swapEndian[32] = DataUnit.swapEndian['J'];
+
+    function DataUnit(header, data) {
+      if (data instanceof ArrayBuffer) {
+        this.buffer = data;
+      } else {
+        this.blob = data;
+      }
+    }
+
+    return DataUnit;
+
+  })(Base);
+
+  this.astro.FITS.DataUnit = DataUnit;
+
+  HeaderVerify = {
+    verifyOrder: function(keyword, order) {
+      if (order !== this.cardIndex) {
+        return console.warn("" + keyword + " should appear at index " + this.cardIndex + " in the FITS header");
+      }
+    },
+    verifyBetween: function(keyword, value, lower, upper) {
+      if (!(value >= lower && value <= upper)) {
+        throw "The " + keyword + " value of " + value + " is not between " + lower + " and " + upper;
+      }
+    },
+    verifyBoolean: function(value) {
+      if (value === "T") {
+        return true;
+      } else {
+        return false;
+      }
+    },
+    VerifyFns: {
+      SIMPLE: function() {
+        var args, value;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        value = arguments[0];
+        this.primary = true;
+        this.verifyOrder("SIMPLE", 0);
+        return this.verifyBoolean(value);
+      },
+      XTENSION: function() {
+        var args;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        this.extension = true;
+        this.extensionType = arguments[0];
+        this.verifyOrder("XTENSION", 0);
+        return this.extensionType;
+      },
+      BITPIX: function() {
+        var args, key, value;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        key = "BITPIX";
+        value = parseInt(arguments[0]);
+        this.verifyOrder(key, 1);
+        if (value !== 8 && value !== 16 && value !== 32 && value !== (-32) && value !== (-64)) {
+          throw "" + key + " value " + value + " is not permitted";
+        }
+        return value;
+      },
+      NAXIS: function() {
+        var args, array, key, required, value, _ref;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        key = "NAXIS";
+        value = parseInt(arguments[0]);
+        array = arguments[1];
+        if (!array) {
+          this.verifyOrder(key, 2);
+          this.verifyBetween(key, value, 0, 999);
+          if (this.isExtension()) {
+            if ((_ref = this.extensionType) === "TABLE" || _ref === "BINTABLE") {
+              required = 2;
+              if (value !== required) {
+                throw "" + key + " must be " + required + " for TABLE and BINTABLE extensions";
+              }
+            }
+          }
+        }
+        return value;
+      },
+      PCOUNT: function() {
+        var args, key, order, required, value, _ref;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        key = "PCOUNT";
+        value = parseInt(arguments[0]);
+        order = 1 + 1 + 1 + this.get("NAXIS");
+        this.verifyOrder(key, order);
+        if (this.isExtension()) {
+          if ((_ref = this.extensionType) === "IMAGE" || _ref === "TABLE") {
+            required = 0;
+            if (value !== required) {
+              throw "" + key + " must be " + required + " for the " + this.extensionType + " extensions";
+            }
+          }
+        }
+        return value;
+      },
+      GCOUNT: function() {
+        var args, key, order, required, value, _ref;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        key = "GCOUNT";
+        value = parseInt(arguments[0]);
+        order = 1 + 1 + 1 + this.get("NAXIS") + 1;
+        this.verifyOrder(key, order);
+        if (this.isExtension()) {
+          if ((_ref = this.extensionType) === "IMAGE" || _ref === "TABLE" || _ref === "BINTABLE") {
+            required = 1;
+            if (value !== required) {
+              throw "" + key + " must be " + required + " for the " + this.extensionType + " extensions";
+            }
+          }
+        }
+        return value;
+      },
+      EXTEND: function() {
+        var args, value;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        value = arguments[0];
+        if (!this.isPrimary()) {
+          throw "EXTEND must only appear in the primary header";
+        }
+        return this.verifyBoolean(value);
+      },
+      BSCALE: function() {
+        var args;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        return parseFloat(arguments[0]);
+      },
+      BZERO: function() {
+        var args;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        return parseFloat(arguments[0]);
+      },
+      BLANK: function() {
+        var args, value;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        value = arguments[0];
+        if (!(this.get("BITPIX") > 0)) {
+          console.warn("BLANK is not to be used for BITPIX = " + (this.get('BITPIX')));
+        }
+        return parseInt(value);
+      },
+      DATAMIN: function() {
+        var args;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        return parseFloat(arguments[0]);
+      },
+      DATAMAX: function() {
+        var args;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        return parseFloat(arguments[0]);
+      },
+      EXTVER: function() {
+        var args;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        return parseInt(arguments[0]);
+      },
+      EXTLEVEL: function() {
+        var args;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        return parseInt(arguments[0]);
+      },
+      TFIELDS: function() {
+        var args, value;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        value = parseInt(arguments[0]);
+        this.verifyBetween("TFIELDS", value, 0, 999);
+        return value;
+      },
+      TBCOL: function() {
+        var args, index, value;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        value = arguments[0];
+        index = arguments[2];
+        this.verifyBetween("TBCOL", index, 0, this.get("TFIELDS"));
+        return value;
+      },
+      ZIMAGE: function() {
+        var args;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        return this.verifyBoolean(arguments[0]);
+      },
+      ZCMPTYPE: function() {
+        var args, value;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        value = arguments[0];
+        if (value !== 'GZIP_1' && value !== 'RICE_1' && value !== 'PLIO_1' && value !== 'HCOMPRESS_1') {
+          throw "ZCMPTYPE value " + value + " is not permitted";
+        }
+        if (value !== 'RICE_1') {
+          throw "Compress type " + value + " is not yet implement";
+        }
+        return value;
+      },
+      ZBITPIX: function() {
+        var args, value;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        value = parseInt(arguments[0]);
+        if (value !== 8 && value !== 16 && value !== 32 && value !== 64 && value !== (-32) && value !== (-64)) {
+          throw "ZBITPIX value " + value + " is not permitted";
+        }
+        return value;
+      },
+      ZNAXIS: function() {
+        var args, array, value;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        value = parseInt(arguments[0]);
+        array = arguments[1];
+        value = value;
+        if (!array) {
+          this.verifyBetween("ZNAXIS", value, 0, 999);
+        }
+        return value;
+      },
+      ZTILE: function() {
+        var args;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        return parseInt(arguments[0]);
+      },
+      ZSIMPLE: function() {
+        var args;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        if (arguments[0] === "T") {
+          return true;
+        } else {
+          return false;
+        }
+      },
+      ZPCOUNT: function() {
+        var args;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        return parseInt(arguments[0]);
+      },
+      ZGCOUNT: function() {
+        var args;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        return parseInt(arguments[0]);
+      },
+      ZDITHER0: function() {
+        var args;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        return parseInt(arguments[0]);
+      }
+    }
+  };
+
+  this.astro.FITS.HeaderVerify = HeaderVerify;
+
+  Header = (function(_super) {
+    __extends(Header, _super);
+
+    Header.include(HeaderVerify);
+
+    Header.prototype.arrayPattern = /(\D+)(\d+)/;
+
+    Header.prototype.maxLines = 600;
+
+    function Header(block) {
+      var method, name, _ref;
+      this.primary = false;
+      this.extension = false;
+      this.verifyCard = {};
+      _ref = this.VerifyFns;
+      for (name in _ref) {
+        method = _ref[name];
+        this.verifyCard[name] = this.proxy(method);
+      }
+      this.cards = {};
+      this.cards["COMMENT"] = [];
+      this.cards["HISTORY"] = [];
+      this.cardIndex = 0;
+      this.block = block;
+      this.readBlock(block);
+    }
+
+    Header.prototype.get = function(key) {
+      if (this.contains(key)) {
+        return this.cards[key].value;
+      } else {
+        return null;
+      }
+    };
+
+    Header.prototype.set = function(key, value, comment) {
+      comment = comment || '';
+      this.cards[key] = {
+        index: this.cardIndex,
+        value: value,
+        comment: comment
+      };
+      return this.cardIndex += 1;
+    };
+
+    Header.prototype.contains = function(key) {
+      return this.cards.hasOwnProperty(key);
+    };
+
+    Header.prototype.readLine = function(l) {
+      var blank, comment, firstByte, indicator, key, value, _ref;
+      key = l.slice(0, 8).trim();
+      blank = key === '';
+      if (blank) {
+        return;
+      }
+      indicator = l.slice(8, 10);
+      value = l.slice(10);
+      if (indicator !== "= ") {
+        if (key === 'COMMENT' || key === 'HISTORY') {
+          this.cards[key].push(value.trim());
+        }
+        return;
+      }
+      _ref = value.split(' /'), value = _ref[0], comment = _ref[1];
+      value = value.trim();
+      firstByte = value[0];
+      if (firstByte === "'") {
+        value = value.slice(1, -1).trim();
+      } else {
+        if (value !== 'T' && value !== 'F') {
+          value = parseFloat(value);
+        }
+      }
+      value = this.validate(key, value);
+      return this.set(key, value, comment);
+    };
+
+    Header.prototype.validate = function(key, value) {
+      var baseKey, index, isArray, match, _ref;
+      index = null;
+      baseKey = key;
+      isArray = this.arrayPattern.test(key);
+      if (isArray) {
+        match = this.arrayPattern.exec(key);
+        _ref = match.slice(1), baseKey = _ref[0], index = _ref[1];
+      }
+      if (baseKey in this.verifyCard) {
+        value = this.verifyCard[baseKey](value, isArray, index);
+      }
+      return value;
+    };
+
+    Header.prototype.readBlock = function(block) {
+      var i, line, lineWidth, nLines, _i, _ref, _results;
+      lineWidth = 80;
+      nLines = block.length / lineWidth;
+      nLines = nLines < this.maxLines ? nLines : this.maxLines;
+      _results = [];
+      for (i = _i = 0, _ref = nLines - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) {
+        line = block.slice(i * lineWidth, (i + 1) * lineWidth);
+        _results.push(this.readLine(line));
+      }
+      return _results;
+    };
+
+    Header.prototype.hasDataUnit = function() {
+      if (this.get("NAXIS") === 0) {
+        return false;
+      } else {
+        return true;
+      }
+    };
+
+    Header.prototype.getDataLength = function() {
+      var i, length, naxis, _i, _ref;
+      if (!this.hasDataUnit()) {
+        return 0;
+      }
+      naxis = [];
+      for (i = _i = 1, _ref = this.get("NAXIS"); 1 <= _ref ? _i <= _ref : _i >= _ref; i = 1 <= _ref ? ++_i : --_i) {
+        naxis.push(this.get("NAXIS" + i));
+      }
+      length = naxis.reduce(function(a, b) {
+        return a * b;
+      }) * Math.abs(this.get("BITPIX")) / 8;
+      length += this.get("PCOUNT");
+      return length;
+    };
+
+    Header.prototype.getDataType = function() {
+      switch (this.extensionType) {
+        case 'BINTABLE':
+          if (this.contains('ZIMAGE')) {
+            return 'CompressedImage';
+          }
+          return 'BinaryTable';
+        case 'TABLE':
+          return 'Table';
+        default:
+          if (this.hasDataUnit()) {
+            return 'Image';
+          } else {
+            return null;
+          }
+      }
+    };
+
+    Header.prototype.isPrimary = function() {
+      return this.primary;
+    };
+
+    Header.prototype.isExtension = function() {
+      return this.extension;
+    };
+
+    return Header;
+
+  })(Base);
+
+  this.astro.FITS.Header = Header;
+
+  ImageUtils = {
+    getExtent: function(arr) {
+      var index, max, min, value;
+      index = arr.length;
+      while (index--) {
+        value = arr[index];
+        if (isNaN(value)) {
+          continue;
+        }
+        min = max = value;
+        break;
+      }
+      if (index === -1) {
+        return [NaN, NaN];
+      }
+      while (index--) {
+        value = arr[index];
+        if (isNaN(value)) {
+          continue;
+        }
+        if (value < min) {
+          min = value;
+        }
+        if (value > max) {
+          max = value;
+        }
+      }
+      return [min, max];
+    },
+    getPixel: function(arr, x, y) {
+      return arr[y * this.width + x];
+    }
+  };
+
+  this.astro.FITS.ImageUtils = ImageUtils;
+
+  Image = (function(_super) {
+    __extends(Image, _super);
+
+    Image.include(ImageUtils);
+
+    Image.prototype.allocationSize = 16777216;
+
+    function Image(header, data) {
+      var begin, frame, i, naxis, _i, _j, _ref;
+      Image.__super__.constructor.apply(this, arguments);
+      naxis = header.get("NAXIS");
+      this.bitpix = header.get("BITPIX");
+      this.naxis = [];
+      for (i = _i = 1; 1 <= naxis ? _i <= naxis : _i >= naxis; i = 1 <= naxis ? ++_i : --_i) {
+        this.naxis.push(header.get("NAXIS" + i));
+      }
+      this.width = header.get("NAXIS1");
+      this.height = header.get("NAXIS2") || 1;
+      this.depth = header.get("NAXIS3") || 1;
+      this.bzero = header.get("BZERO") || 0;
+      this.bscale = header.get("BSCALE") || 1;
+      this.bytes = Math.abs(this.bitpix) / 8;
+      this.length = this.naxis.reduce(function(a, b) {
+        return a * b;
+      }) * Math.abs(this.bitpix) / 8;
+      this.frame = 0;
+      this.frameOffsets = [];
+      this.frameLength = this.bytes * this.width * this.height;
+      this.nBuffers = this.buffer != null ? 1 : 2;
+      for (i = _j = 0, _ref = this.depth - 1; 0 <= _ref ? _j <= _ref : _j >= _ref; i = 0 <= _ref ? ++_j : --_j) {
+        begin = i * this.frameLength;
+        frame = {
+          begin: begin
+        };
+        if (this.buffer != null) {
+          frame.buffers = [this.buffer.slice(begin, begin + this.frameLength)];
+        }
+        this.frameOffsets.push(frame);
+      }
+    }
+
+    Image.prototype._getFrame = function(buffer, bitpix, bzero, bscale) {
+      var arr, bytes, dataType, i, nPixels, swapEndian, tmp, value;
+      bytes = Math.abs(bitpix) / 8;
+      nPixels = i = buffer.byteLength / bytes;
+      dataType = Math.abs(bitpix);
+      if (bitpix > 0) {
+        switch (bitpix) {
+          case 8:
+            tmp = new Uint8Array(buffer);
+            tmp = new Uint16Array(tmp);
+            swapEndian = function(value) {
+              return value;
+            };
+            break;
+          case 16:
+            tmp = new Int16Array(buffer);
+            swapEndian = function(value) {
+              return ((value & 0xFF) << 8) | ((value >> 8) & 0xFF);
+            };
+            break;
+          case 32:
+            tmp = new Int32Array(buffer);
+            swapEndian = function(value) {
+              return ((value & 0xFF) << 24) | ((value & 0xFF00) << 8) | ((value >> 8) & 0xFF00) | ((value >> 24) & 0xFF);
+            };
+        }
+        if (!(parseInt(bzero) === bzero && parseInt(bscale) === bscale)) {
+          arr = new Float32Array(tmp.length);
+        } else {
+          arr = tmp;
+        }
+        while (nPixels--) {
+          tmp[nPixels] = swapEndian(tmp[nPixels]);
+          arr[nPixels] = bzero + bscale * tmp[nPixels];
+        }
+      } else {
+        arr = new Uint32Array(buffer);
+        swapEndian = function(value) {
+          return ((value & 0xFF) << 24) | ((value & 0xFF00) << 8) | ((value >> 8) & 0xFF00) | ((value >> 24) & 0xFF);
+        };
+        while (i--) {
+          value = arr[i];
+          arr[i] = swapEndian(value);
+        }
+        arr = new Float32Array(buffer);
+        while (nPixels--) {
+          arr[nPixels] = bzero + bscale * arr[nPixels];
+        }
+      }
+      return arr;
+    };
+
+    Image.prototype._getFrameAsync = function(buffers, callback, opts) {
+      var URL, blobGetFrame, blobOnMessage, fn1, fn2, i, mime, msg, onmessage, pixels, start, urlGetFrame, urlOnMessage, worker,
+        _this = this;
+      onmessage = function(e) {
+        var arr, bitpix, bscale, buffer, bzero, data, url;
+        data = e.data;
+        buffer = data.buffer;
+        bitpix = data.bitpix;
+        bzero = data.bzero;
+        bscale = data.bscale;
+        url = data.url;
+        importScripts(url);
+        arr = _getFrame(buffer, bitpix, bzero, bscale);
+        return postMessage(arr);
+      };
+      fn1 = onmessage.toString().replace('return postMessage', 'postMessage');
+      fn1 = "onmessage = " + fn1;
+      fn2 = this._getFrame.toString();
+      fn2 = fn2.replace('function', 'function _getFrame');
+      mime = "application/javascript";
+      blobOnMessage = new Blob([fn1], {
+        type: mime
+      });
+      blobGetFrame = new Blob([fn2], {
+        type: mime
+      });
+      URL = window.URL || window.webkitURL;
+      urlOnMessage = URL.createObjectURL(blobOnMessage);
+      urlGetFrame = URL.createObjectURL(blobGetFrame);
+      worker = new Worker(urlOnMessage);
+      msg = {
+        buffer: buffers[0],
+        bitpix: this.bitpix,
+        bzero: this.bzero,
+        bscale: this.bscale,
+        url: urlGetFrame
+      };
+      i = 0;
+      pixels = null;
+      start = 0;
+      worker.onmessage = function(e) {
+        var arr;
+        arr = e.data;
+        if (pixels == null) {
+          pixels = new arr.constructor(_this.width * _this.height);
+        }
+        pixels.set(arr, start);
+        start += arr.length;
+        i += 1;
+        if (i === _this.nBuffers) {
+          _this.invoke(callback, opts, pixels);
+          URL.revokeObjectURL(urlOnMessage);
+          URL.revokeObjectURL(urlGetFrame);
+          return worker.terminate();
+        } else {
+          msg.buffer = buffers[i];
+          return worker.postMessage(msg, [buffers[i]]);
+        }
+      };
+      worker.postMessage(msg, [buffers[0]]);
+    };
+
+    Image.prototype.getFrame = function(frame, callback, opts) {
+      var begin, blobFrame, blobs, buffers, bytesPerBuffer, frameInfo, i, nRowsPerBuffer, reader, start, _i, _ref,
+        _this = this;
+      this.frame = frame || this.frame;
+      frameInfo = this.frameOffsets[this.frame];
+      buffers = frameInfo.buffers;
+      if ((buffers != null ? buffers.length : void 0) === this.nBuffers) {
+        return this._getFrameAsync(buffers, callback, opts);
+      } else {
+        this.frameOffsets[this.frame].buffers = [];
+        begin = frameInfo.begin;
+        blobFrame = this.blob.slice(begin, begin + this.frameLength);
+        blobs = [];
+        nRowsPerBuffer = Math.floor(this.height / this.nBuffers);
+        bytesPerBuffer = nRowsPerBuffer * this.bytes * this.width;
+        for (i = _i = 0, _ref = this.nBuffers - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) {
+          start = i * bytesPerBuffer;
+          if (i === this.nBuffers - 1) {
+            blobs.push(blobFrame.slice(start));
+          } else {
+            blobs.push(blobFrame.slice(start, start + bytesPerBuffer));
+          }
+        }
+        buffers = [];
+        reader = new FileReader();
+        reader.frame = this.frame;
+        i = 0;
+        reader.onloadend = function(e) {
+          var buffer;
+          frame = e.target.frame;
+          buffer = e.target.result;
+          _this.frameOffsets[frame].buffers.push(buffer);
+          i += 1;
+          if (i === _this.nBuffers) {
+            return _this.getFrame(frame, callback, opts);
+          } else {
+            return reader.readAsArrayBuffer(blobs[i]);
+          }
+        };
+        return reader.readAsArrayBuffer(blobs[0]);
+      }
+    };
+
+    Image.prototype.getFrames = function(frame, number, callback, opts) {
+      var cb,
+        _this = this;
+      cb = function(arr, opts) {
+        _this.invoke(callback, opts, arr);
+        number -= 1;
+        frame += 1;
+        if (!number) {
+          return;
+        }
+        return _this.getFrame(frame, cb, opts);
+      };
+      return this.getFrame(frame, cb, opts);
+    };
+
+    Image.prototype.isDataCube = function() {
+      if (this.naxis.length > 2) {
+        return true;
+      } else {
+        return false;
+      }
+    };
+
+    return Image;
+
+  })(DataUnit);
+
+  this.astro.FITS.Image = Image;
+
+  Tabular = (function(_super) {
+    __extends(Tabular, _super);
+
+    Tabular.prototype.maxMemory = 1048576;
+
+    function Tabular(header, data) {
+      Tabular.__super__.constructor.apply(this, arguments);
+      this.rowByteSize = header.get("NAXIS1");
+      this.rows = header.get("NAXIS2");
+      this.cols = header.get("TFIELDS");
+      this.length = this.rowByteSize * this.rows;
+      this.heapLength = header.get("PCOUNT");
+      this.columns = this.getColumns(header);
+      if (this.buffer != null) {
+        this.rowsInMemory = this._rowsInMemoryBuffer;
+        this.heap = this.buffer.slice(this.length, this.length + this.heapLength);
+      } else {
+        this.rowsInMemory = this._rowsInMemoryBlob;
+        this.firstRowInBuffer = this.lastRowInBuffer = 0;
+        this.nRowsInBuffer = Math.floor(this.maxMemory / this.rowByteSize);
+      }
+      this.accessors = [];
+      this.descriptors = [];
+      this.elementByteLengths = [];
+      this.setAccessors(header);
+    }
+
+    Tabular.prototype._rowsInMemoryBuffer = function() {
+      return true;
+    };
+
+    Tabular.prototype._rowsInMemoryBlob = function(firstRow, lastRow) {
+      if (firstRow < this.firstRowInBuffer) {
+        return false;
+      }
+      if (lastRow > this.lastRowInBuffer) {
+        return false;
+      }
+      return true;
+    };
+
+    Tabular.prototype.getColumns = function(header) {
+      var columns, i, key, _i, _ref;
+      columns = [];
+      for (i = _i = 1, _ref = this.cols; 1 <= _ref ? _i <= _ref : _i >= _ref; i = 1 <= _ref ? ++_i : --_i) {
+        key = "TTYPE" + i;
+        if (!header.contains(key)) {
+          return null;
+        }
+        columns.push(header.get(key));
+      }
+      return columns;
+    };
+
+    Tabular.prototype.getColumn = function(name, callback, opts) {
+      var accessor, cb, column, descriptor, elementByteLength, elementByteOffset, factor, i, index, iterations, rowsPerIteration,
+        _this = this;
+      if (this.blob != null) {
+        index = this.columns.indexOf(name);
+        descriptor = this.descriptors[index];
+        accessor = this.accessors[index];
+        elementByteLength = this.elementByteLengths[index];
+        elementByteOffset = this.elementByteLengths.slice(0, index);
+        if (elementByteOffset.length === 0) {
+          elementByteOffset = 0;
+        } else {
+          elementByteOffset = elementByteOffset.reduce(function(a, b) {
+            return a + b;
+          });
+        }
+        column = this.typedArray[descriptor] != null ? new this.typedArray[descriptor](this.rows) : [];
+        rowsPerIteration = ~~(this.maxMemory / this.rowByteSize);
+        rowsPerIteration = Math.min(rowsPerIteration, this.rows);
+        factor = this.rows / rowsPerIteration;
+        iterations = Math.floor(factor) === factor ? factor : Math.floor(factor) + 1;
+        i = 0;
+        index = 0;
+        cb = function(buffer, opts) {
+          var nRows, offset, startRow, view;
+          nRows = buffer.byteLength / _this.rowByteSize;
+          view = new DataView(buffer);
+          offset = elementByteOffset;
+          while (nRows--) {
+            column[i] = accessor(view, offset)[0];
+            i += 1;
+            offset += _this.rowByteSize;
+          }
+          iterations -= 1;
+          index += 1;
+          if (iterations) {
+            startRow = index * rowsPerIteration;
+            return _this.getTableBuffer(startRow, rowsPerIteration, cb, opts);
+          } else {
+            _this.invoke(callback, opts, column);
+          }
+        };
+        return this.getTableBuffer(0, rowsPerIteration, cb, opts);
+      } else {
+        cb = function(rows, opts) {
+          column = rows.map(function(d) {
+            return d[name];
+          });
+          return _this.invoke(callback, opts, column);
+        };
+        return this.getRows(0, this.rows, cb, opts);
+      }
+    };
+
+    Tabular.prototype.getTableBuffer = function(row, number, callback, opts) {
+      var begin, blobRows, end, reader,
+        _this = this;
+      number = Math.min(this.rows - row, number);
+      begin = row * this.rowByteSize;
+      end = begin + number * this.rowByteSize;
+      blobRows = this.blob.slice(begin, end);
+      reader = new FileReader();
+      reader.row = row;
+      reader.number = number;
+      reader.onloadend = function(e) {
+        return _this.invoke(callback, opts, e.target.result);
+      };
+      return reader.readAsArrayBuffer(blobRows);
+    };
+
+    Tabular.prototype.getRows = function(row, number, callback, opts) {
+      var begin, blobRows, buffer, end, reader, rows,
+        _this = this;
+      if (this.rowsInMemory(row, row + number)) {
+        if (this.blob != null) {
+          buffer = this.buffer;
+        } else {
+          begin = row * this.rowByteSize;
+          end = begin + number * this.rowByteSize;
+          buffer = this.buffer.slice(begin, end);
+        }
+        rows = this._getRows(buffer, number);
+        this.invoke(callback, opts, rows);
+        return rows;
+      } else {
+        begin = row * this.rowByteSize;
+        end = begin + Math.max(this.nRowsInBuffer * this.rowByteSize, number * this.rowByteSize);
+        blobRows = this.blob.slice(begin, end);
+        reader = new FileReader();
+        reader.row = row;
+        reader.number = number;
+        reader.onloadend = function(e) {
+          var target;
+          target = e.target;
+          _this.buffer = target.result;
+          _this.firstRowInBuffer = _this.lastRowInBuffer = target.row;
+          _this.lastRowInBuffer += target.number;
+          return _this.getRows(row, number, callback, opts);
+        };
+        return reader.readAsArrayBuffer(blobRows);
+      }
+    };
+
+    return Tabular;
+
+  })(DataUnit);
+
+  this.astro.FITS.Tabular = Tabular;
+
+  Table = (function(_super) {
+    __extends(Table, _super);
+
+    function Table() {
+      _ref = Table.__super__.constructor.apply(this, arguments);
+      return _ref;
+    }
+
+    Table.prototype.dataAccessors = {
+      A: function(value) {
+        return value.trim();
+      },
+      I: function(value) {
+        return parseInt(value);
+      },
+      F: function(value) {
+        return parseFloat(value);
+      },
+      E: function(value) {
+        return parseFloat(value);
+      },
+      D: function(value) {
+        return parseFloat(value);
+      }
+    };
+
+    Table.prototype.setAccessors = function(header) {
+      var descriptor, form, i, match, pattern, type, _i, _ref1, _results,
+        _this = this;
+      pattern = /([AIFED])(\d+)\.*(\d+)*/;
+      _results = [];
+      for (i = _i = 1, _ref1 = this.cols; 1 <= _ref1 ? _i <= _ref1 : _i >= _ref1; i = 1 <= _ref1 ? ++_i : --_i) {
+        form = header.get("TFORM" + i);
+        type = header.get("TTYPE" + i);
+        match = pattern.exec(form);
+        descriptor = match[1];
+        _results.push((function(descriptor) {
+          var accessor;
+          accessor = function(value) {
+            return _this.dataAccessors[descriptor](value);
+          };
+          return _this.accessors.push(accessor);
+        })(descriptor));
+      }
+      return _results;
+    };
+
+    Table.prototype._getRows = function(buffer) {
+      var accessor, arr, begin, end, i, index, line, nRows, row, rows, subarray, value, _i, _j, _k, _len, _len1, _ref1, _ref2;
+      nRows = buffer.byteLength / this.rowByteSize;
+      arr = new Uint8Array(buffer);
+      rows = [];
+      for (i = _i = 0, _ref1 = nRows - 1; 0 <= _ref1 ? _i <= _ref1 : _i >= _ref1; i = 0 <= _ref1 ? ++_i : --_i) {
+        begin = i * this.rowByteSize;
+        end = begin + this.rowByteSize;
+        subarray = arr.subarray(begin, end);
+        line = '';
+        for (_j = 0, _len = subarray.length; _j < _len; _j++) {
+          value = subarray[_j];
+          line += String.fromCharCode(value);
+        }
+        line = line.trim().split(/\s+/);
+        row = {};
+        _ref2 = this.accessors;
+        for (index = _k = 0, _len1 = _ref2.length; _k < _len1; index = ++_k) {
+          accessor = _ref2[index];
+          value = line[index];
+          row[this.columns[index]] = accessor(value);
+        }
+        rows.push(row);
+      }
+      return rows;
+    };
+
+    return Table;
+
+  })(Tabular);
+
+  this.astro.FITS.Table = Table;
+
+  BinaryTable = (function(_super) {
+    __extends(BinaryTable, _super);
+
+    function BinaryTable() {
+      _ref1 = BinaryTable.__super__.constructor.apply(this, arguments);
+      return _ref1;
+    }
+
+    BinaryTable.prototype.typedArray = {
+      B: Uint8Array,
+      I: Uint16Array,
+      J: Uint32Array,
+      E: Float32Array,
+      D: Float64Array,
+      1: Uint8Array,
+      2: Uint16Array,
+      4: Uint32Array
+    };
+
+    BinaryTable.offsets = {
+      L: 1,
+      B: 1,
+      I: 2,
+      J: 4,
+      K: 8,
+      A: 1,
+      E: 4,
+      D: 8,
+      C: 8,
+      M: 16
+    };
+
+    BinaryTable.prototype.dataAccessors = {
+      L: function(view, offset) {
+        var val, x;
+        x = view.getInt8(offset);
+        offset += 1;
+        val = x === 84 ? true : false;
+        return [val, offset];
+      },
+      B: function(view, offset) {
+        var val;
+        val = view.getUint8(offset);
+        offset += 1;
+        return [val, offset];
+      },
+      I: function(view, offset) {
+        var val;
+        val = view.getInt16(offset);
+        offset += 2;
+        return [val, offset];
+      },
+      J: function(view, offset) {
+        var val;
+        val = view.getInt32(offset);
+        offset += 4;
+        return [val, offset];
+      },
+      K: function(view, offset) {
+        var factor, highByte, lowByte, mod, val;
+        highByte = Math.abs(view.getInt32(offset));
+        offset += 4;
+        lowByte = Math.abs(view.getInt32(offset));
+        offset += 4;
+        mod = highByte % 10;
+        factor = mod ? -1 : 1;
+        highByte -= mod;
+        val = factor * ((highByte << 32) | lowByte);
+        return [val, offset];
+      },
+      A: function(view, offset) {
+        var val;
+        val = view.getUint8(offset);
+        val = String.fromCharCode(val);
+        offset += 1;
+        return [val, offset];
+      },
+      E: function(view, offset) {
+        var val;
+        val = view.getFloat32(offset);
+        offset += 4;
+        return [val, offset];
+      },
+      D: function(view, offset) {
+        var val;
+        val = view.getFloat64(offset);
+        offset += 8;
+        return [val, offset];
+      },
+      C: function(view, offset) {
+        var val, val1, val2;
+        val1 = view.getFloat32(offset);
+        offset += 4;
+        val2 = view.getFloat32(offset);
+        offset += 4;
+        val = [val1, val2];
+        return [val, offset];
+      },
+      M: function(view, offset) {
+        var val, val1, val2;
+        val1 = view.getFloat64(offset);
+        offset += 8;
+        val2 = view.getFloat64(offset);
+        offset += 8;
+        val = [val1, val2];
+        return [val, offset];
+      }
+    };
+
+    BinaryTable.prototype.toBits = function(byte) {
+      var arr, i;
+      arr = [];
+      i = 128;
+      while (i >= 1) {
+        arr.push((byte & i ? 1 : 0));
+        i /= 2;
+      }
+      return arr;
+    };
+
+    BinaryTable.prototype.getFromHeap = function(view, offset, descriptor) {
+      var arr, heapOffset, heapSlice, i, length;
+      length = view.getInt32(offset);
+      offset += 4;
+      heapOffset = view.getInt32(offset);
+      offset += 4;
+      heapSlice = this.heap.slice(heapOffset, heapOffset + length);
+      arr = new this.typedArray[descriptor](heapSlice);
+      i = arr.length;
+      while (i--) {
+        arr[i] = this.constructor.swapEndian[descriptor](arr[i]);
+      }
+      return [arr, offset];
+    };
+
+    BinaryTable.prototype.setAccessors = function(header) {
+      var count, descriptor, form, i, isArray, match, pattern, type, _i, _ref2, _results,
+        _this = this;
+      pattern = /(\d*)([P|Q]*)([L|X|B|I|J|K|A|E|D|C|M]{1})/;
+      _results = [];
+      for (i = _i = 1, _ref2 = this.cols; 1 <= _ref2 ? _i <= _ref2 : _i >= _ref2; i = 1 <= _ref2 ? ++_i : --_i) {
+        form = header.get("TFORM" + i);
+        type = header.get("TTYPE" + i);
+        match = pattern.exec(form);
+        count = parseInt(match[1]) || 1;
+        isArray = match[2];
+        descriptor = match[3];
+        _results.push((function(descriptor, count) {
+          var accessor, nBytes;
+          _this.descriptors.push(descriptor);
+          _this.elementByteLengths.push(_this.constructor.offsets[descriptor] * count);
+          if (isArray) {
+            switch (type) {
+              case "COMPRESSED_DATA":
+                accessor = function(view, offset) {
+                  var arr, pixels, _ref3;
+                  _ref3 = _this.getFromHeap(view, offset, descriptor), arr = _ref3[0], offset = _ref3[1];
+                  pixels = new _this.typedArray[_this.algorithmParameters["BYTEPIX"]](_this.ztile[0]);
+                  Decompress.Rice(arr, _this.algorithmParameters["BLOCKSIZE"], _this.algorithmParameters["BYTEPIX"], pixels, _this.ztile[0], Decompress.RiceSetup);
+                  return [pixels, offset];
+                };
+                break;
+              case "GZIP_COMPRESSED_DATA":
+                accessor = function(view, offset) {
+                  var arr;
+                  arr = new Float32Array(_this.width);
+                  i = arr.length;
+                  while (i--) {
+                    arr[i] = NaN;
+                  }
+                  return [arr, offset];
+                };
+                break;
+              default:
+                accessor = function(view, offset) {
+                  return _this.getFromHeap(view, offset, descriptor);
+                };
+            }
+          } else {
+            if (count === 1) {
+              accessor = function(view, offset) {
+                var value, _ref3;
+                _ref3 = _this.dataAccessors[descriptor](view, offset), value = _ref3[0], offset = _ref3[1];
+                return [value, offset];
+              };
+            } else {
+              if (descriptor === 'X') {
+                nBytes = Math.log(count) / Math.log(2);
+                accessor = function(view, offset) {
+                  var arr, bits, buffer, byte, bytes, _j, _len;
+                  buffer = view.buffer.slice(offset, offset + nBytes);
+                  bytes = new Uint8Array(buffer);
+                  bits = [];
+                  for (_j = 0, _len = bytes.length; _j < _len; _j++) {
+                    byte = bytes[_j];
+                    arr = _this.toBits(byte);
+                    bits = bits.concat(arr);
+                  }
+                  offset += nBytes;
+                  return [bits.slice(0, +(count - 1) + 1 || 9e9), offset];
+                };
+              } else if (descriptor === 'A') {
+                accessor = function(view, offset) {
+                  var arr, buffer, s, value, _j, _len;
+                  buffer = view.buffer.slice(offset, offset + count);
+                  arr = new Uint8Array(buffer);
+                  s = '';
+                  for (_j = 0, _len = arr.length; _j < _len; _j++) {
+                    value = arr[_j];
+                    s += String.fromCharCode(value);
+                  }
+                  s = s.trim();
+                  offset += count;
+                  return [s, offset];
+                };
+              } else {
+                accessor = function(view, offset) {
+                  var data, value, _ref3;
+                  i = count;
+                  data = [];
+                  while (i--) {
+                    _ref3 = _this.dataAccessors[descriptor](view, offset), value = _ref3[0], offset = _ref3[1];
+                    data.push(value);
+                  }
+                  return [data, offset];
+                };
+              }
+            }
+          }
+          return _this.accessors.push(accessor);
+        })(descriptor, count));
+      }
+      return _results;
+    };
+
+    BinaryTable.prototype._getRows = function(buffer, nRows) {
+      var accessor, index, offset, row, rows, value, view, _i, _len, _ref2, _ref3;
+      view = new DataView(buffer);
+      offset = 0;
+      rows = [];
+      while (nRows--) {
+        row = {};
+        _ref2 = this.accessors;
+        for (index = _i = 0, _len = _ref2.length; _i < _len; index = ++_i) {
+          accessor = _ref2[index];
+          _ref3 = accessor(view, offset), value = _ref3[0], offset = _ref3[1];
+          row[this.columns[index]] = value;
+        }
+        rows.push(row);
+      }
+      return rows;
+    };
+
+    return BinaryTable;
+
+  })(Tabular);
+
+  this.astro.FITS.BinaryTable = BinaryTable;
+
+  Decompress = {
+    RiceSetup: {
+      1: function(array) {
+        var fsbits, fsmax, lastpix, pointer;
+        pointer = 1;
+        fsbits = 3;
+        fsmax = 6;
+        lastpix = array[0];
+        return [fsbits, fsmax, lastpix, pointer];
+      },
+      2: function(array) {
+        var bytevalue, fsbits, fsmax, lastpix, pointer;
+        pointer = 2;
+        fsbits = 4;
+        fsmax = 14;
+        lastpix = 0;
+        bytevalue = array[0];
+        lastpix = lastpix | (bytevalue << 8);
+        bytevalue = array[1];
+        lastpix = lastpix | bytevalue;
+        return [fsbits, fsmax, lastpix, pointer];
+      },
+      4: function(array) {
+        var bytevalue, fsbits, fsmax, lastpix, pointer;
+        pointer = 4;
+        fsbits = 5;
+        fsmax = 25;
+        lastpix = 0;
+        bytevalue = array[0];
+        lastpix = lastpix | (bytevalue << 24);
+        bytevalue = array[1];
+        lastpix = lastpix | (bytevalue << 16);
+        bytevalue = array[2];
+        lastpix = lastpix | (bytevalue << 8);
+        bytevalue = array[3];
+        lastpix = lastpix | bytevalue;
+        return [fsbits, fsmax, lastpix, pointer];
+      }
+    },
+    Rice: function(array, blocksize, bytepix, pixels, nx, setup) {
+      var b, bbits, diff, fs, fsbits, fsmax, i, imax, k, lastpix, nbits, nonzeroCount, nzero, pointer, _ref2, _ref3;
+      bbits = 1 << fsbits;
+      _ref2 = setup[bytepix](array), fsbits = _ref2[0], fsmax = _ref2[1], lastpix = _ref2[2], pointer = _ref2[3];
+      nonzeroCount = new Uint8Array(256);
+      nzero = 8;
+      _ref3 = [128, 255], k = _ref3[0], i = _ref3[1];
+      while (i >= 0) {
+        while (i >= k) {
+          nonzeroCount[i] = nzero;
+          i -= 1;
+        }
+        k = k / 2;
+        nzero -= 1;
+      }
+      nonzeroCount[0] = 0;
+      b = array[pointer++];
+      nbits = 8;
+      i = 0;
+      while (i < nx) {
+        nbits -= fsbits;
+        while (nbits < 0) {
+          b = (b << 8) | array[pointer++];
+          nbits += 8;
+        }
+        fs = (b >> nbits) - 1;
+        b &= (1 << nbits) - 1;
+        imax = i + blocksize;
+        if (imax > nx) {
+          imax = nx;
+        }
+        if (fs < 0) {
+          while (i < imax) {
+            pixels[i] = lastpix;
+            i += 1;
+          }
+        } else if (fs === fsmax) {
+          while (i < imax) {
+            k = bbits - nbits;
+            diff = b << k;
+            k -= 8;
+            while (k >= 0) {
+              b = array[pointer++];
+              diff |= b << k;
+              k -= 8;
+            }
+            if (nbits > 0) {
+              b = array[pointer++];
+              diff |= b >> (-k);
+              b &= (1 << nbits) - 1;
+            } else {
+              b = 0;
+            }
+            if ((diff & 1) === 0) {
+              diff = diff >> 1;
+            } else {
+              diff = ~(diff >> 1);
+            }
+            pixels[i] = diff + lastpix;
+            lastpix = pixels[i];
+            i++;
+          }
+        } else {
+          while (i < imax) {
+            while (b === 0) {
+              nbits += 8;
+              b = array[pointer++];
+            }
+            nzero = nbits - nonzeroCount[b];
+            nbits -= nzero + 1;
+            b ^= 1 << nbits;
+            nbits -= fs;
+            while (nbits < 0) {
+              b = (b << 8) | array[pointer++];
+              nbits += 8;
+            }
+            diff = (nzero << fs) | (b >> nbits);
+            b &= (1 << nbits) - 1;
+            if ((diff & 1) === 0) {
+              diff = diff >> 1;
+            } else {
+              diff = ~(diff >> 1);
+            }
+            pixels[i] = diff + lastpix;
+            lastpix = pixels[i];
+            i++;
+          }
+        }
+      }
+      return pixels;
+    }
+  };
+
+  this.astro.FITS.Decompress = Decompress;
+
+  CompressedImage = (function(_super) {
+    __extends(CompressedImage, _super);
+
+    CompressedImage.include(ImageUtils);
+
+    CompressedImage.extend(Decompress);
+
+    CompressedImage.randomGenerator = function() {
+      var a, i, m, random, seed, temp, _i;
+      a = 16807;
+      m = 2147483647;
+      seed = 1;
+      random = new Float32Array(10000);
+      for (i = _i = 0; _i <= 9999; i = ++_i) {
+        temp = a * seed;
+        seed = temp - m * parseInt(temp / m);
+        random[i] = seed / m;
+      }
+      return random;
+    };
+
+    CompressedImage.randomSequence = CompressedImage.randomGenerator();
+
+    function CompressedImage(header, data) {
+      var i, key, value, ztile, _i, _ref2;
+      CompressedImage.__super__.constructor.apply(this, arguments);
+      this.zcmptype = header.get("ZCMPTYPE");
+      this.zbitpix = header.get("ZBITPIX");
+      this.znaxis = header.get("ZNAXIS");
+      this.zblank = header.get("ZBLANK");
+      this.blank = header.get("BLANK");
+      this.zdither = header.get('ZDITHER0') || 0;
+      this.ztile = [];
+      for (i = _i = 1, _ref2 = this.znaxis; 1 <= _ref2 ? _i <= _ref2 : _i >= _ref2; i = 1 <= _ref2 ? ++_i : --_i) {
+        ztile = header.contains("ZTILE" + i) ? header.get("ZTILE" + i) : i === 1 ? header.get("ZNAXIS1") : 1;
+        this.ztile.push(ztile);
+      }
+      this.width = header.get("ZNAXIS1");
+      this.height = header.get("ZNAXIS2") || 1;
+      this.algorithmParameters = {};
+      if (this.zcmptype === 'RICE_1') {
+        this.algorithmParameters["BLOCKSIZE"] = 32;
+        this.algorithmParameters["BYTEPIX"] = 4;
+      }
+      i = 1;
+      while (true) {
+        key = "ZNAME" + i;
+        if (!header.contains(key)) {
+          break;
+        }
+        value = "ZVAL" + i;
+        this.algorithmParameters[header.get(key)] = header.get(value);
+        i += 1;
+      }
+      this.zmaskcmp = header.get("ZMASKCMP");
+      this.zquantiz = header.get("ZQUANTIZ") || "LINEAR_SCALING";
+      this.bzero = header.get("BZERO") || 0;
+      this.bscale = header.get("BSCALE") || 1;
+    }
+
+    CompressedImage.prototype._getRows = function(buffer, nRows) {
+      var accessor, arr, blank, data, i, index, nTile, offset, r, rIndex, row, scale, seed0, seed1, value, view, zero, _i, _j, _len, _len1, _ref2, _ref3;
+      view = new DataView(buffer);
+      offset = 0;
+      arr = new Float32Array(this.width * this.height);
+      while (nRows--) {
+        row = {};
+        _ref2 = this.accessors;
+        for (index = _i = 0, _len = _ref2.length; _i < _len; index = ++_i) {
+          accessor = _ref2[index];
+          _ref3 = accessor(view, offset), value = _ref3[0], offset = _ref3[1];
+          row[this.columns[index]] = value;
+        }
+        data = row['COMPRESSED_DATA'] || row['UNCOMPRESSED_DATA'] || row['GZIP_COMPRESSED_DATA'];
+        blank = row['ZBLANK'] || this.zblank;
+        scale = row['ZSCALE'] || this.bscale;
+        zero = row['ZZERO'] || this.bzero;
+        nTile = this.height - nRows;
+        seed0 = nTile + this.zdither - 1;
+        seed1 = (seed0 - 1) % 10000;
+        rIndex = parseInt(this.constructor.randomSequence[seed1] * 500);
+        for (index = _j = 0, _len1 = data.length; _j < _len1; index = ++_j) {
+          value = data[index];
+          i = (nTile - 1) * this.width + index;
+          if (value === -2147483647) {
+            arr[i] = NaN;
+          } else if (value === -2147483646) {
+            arr[i] = 0;
+          } else {
+            r = this.constructor.randomSequence[rIndex];
+            arr[i] = (value - r + 0.5) * scale + zero;
+          }
+          rIndex += 1;
+          if (rIndex === 10000) {
+            seed1 = (seed1 + 1) % 10000;
+            rIndex = parseInt(this.randomSequence[seed1] * 500);
+          }
+        }
+      }
+      return arr;
+    };
+
+    CompressedImage.prototype.getFrame = function(nFrame, callback, opts) {
+      var heapBlob, reader,
+        _this = this;
+      if (this.heap) {
+        this.frame = nFrame || this.frame;
+        return this.getRows(0, this.rows, callback, opts);
+      } else {
+        heapBlob = this.blob.slice(this.length, this.length + this.heapLength);
+        reader = new FileReader();
+        reader.onloadend = function(e) {
+          _this.heap = e.target.result;
+          return _this.getFrame(nFrame, callback, opts);
+        };
+        return reader.readAsArrayBuffer(heapBlob);
+      }
+    };
+
+    return CompressedImage;
+
+  })(BinaryTable);
+
+  this.astro.FITS.CompressedImage = CompressedImage;
+
+  HDU = (function() {
+    function HDU(header, data) {
+      this.header = header;
+      this.data = data;
+    }
+
+    HDU.prototype.hasData = function() {
+      if (this.data != null) {
+        return true;
+      } else {
+        return false;
+      }
+    };
+
+    return HDU;
+
+  })();
+
+  this.astro.FITS.HDU = HDU;
+
+}).call(this);
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File MOC
+ *
+ * This class represents a MOC (Multi Order Coverage map) layer
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+MOC = (function() {
+    MOC = function(options) {
+        this.order = undefined;
+
+        this.type = 'moc';
+
+        // TODO homogenize options parsing for all kind of overlay (footprints, catalog, MOC)
+        options = options || {};
+        this.name = options.name || "MOC";
+        this.color = options.color || Color.getNextColor();
+        this.opacity = options.opacity || 1;
+        this.opacity = Math.max(0, Math.min(1, this.opacity)); // 0 <= this.opacity <= 1
+        this.lineWidth = options["lineWidth"] || 1;
+        this.adaptativeDisplay = options['adaptativeDisplay'] !== false;
+
+        this.proxyCalled = false; // this is a flag to check whether we already tried to load the MOC through the proxy
+
+        // index of MOC cells at high and low resolution
+        this._highResIndexOrder3 = new Array(768);
+        this._lowResIndexOrder3 = new Array(768);
+        for (var k=0; k<768; k++) {
+            this._highResIndexOrder3[k] = {};
+            this._lowResIndexOrder3[k] = {};
+        }
+
+        this.nbCellsDeepestLevel = 0; // needed to compute the sky fraction of the MOC
+
+        this.isShowing = true;
+        this.ready = false;
+    }
+
+
+    function log2(val) {
+        return Math.log(val) / Math.LN2;
+    }
+
+    // max norder we can currently handle (limitation of healpix.js)
+    MOC.MAX_NORDER = 13; // NSIDE = 8192
+
+    MOC.LOWRES_MAXORDER = 6; // 5 or 6 ??
+    MOC.HIGHRES_MAXORDER = 11; // ??
+
+    // TODO: options to modifiy this ?
+    MOC.PIVOT_FOV = 30; // when do we switch from low res cells to high res cells (fov in degrees)
+
+    // at end of parsing, we need to remove duplicates from the 2 indexes
+    MOC.prototype._removeDuplicatesFromIndexes = function() {
+        var a, aDedup;
+        for (var k=0; k<768; k++) {
+            for (var key in this._highResIndexOrder3[k]) {
+                a = this._highResIndexOrder3[k][key];
+                aDedup = uniq(a);
+                this._highResIndexOrder3[k][key] = aDedup;
+            }
+            for (var key in this._lowResIndexOrder3[k]) {
+                a = this._lowResIndexOrder3[k][key];
+                aDedup = uniq(a);
+                this._lowResIndexOrder3[k][key] = aDedup;
+            }
+        }
+
+    }
+
+    // add pixel (order, ipix)
+    MOC.prototype._addPix = function(order, ipix) {
+        var ipixOrder3 = Math.floor( ipix * Math.pow(4, (3 - order)) );
+        // fill low and high level cells
+        // 1. if order <= LOWRES_MAXORDER, just store value in low and high res cells
+        if (order<=MOC.LOWRES_MAXORDER) {
+            if (! (order in this._lowResIndexOrder3[ipixOrder3])) {
+                this._lowResIndexOrder3[ipixOrder3][order] = [];
+                this._highResIndexOrder3[ipixOrder3][order] = [];
+            }
+            this._lowResIndexOrder3[ipixOrder3][order].push(ipix);
+            this._highResIndexOrder3[ipixOrder3][order].push(ipix);
+        }
+        // 2. if LOWRES_MAXORDER < order <= HIGHRES_MAXORDER , degrade ipix for low res cells
+        else if (order<=MOC.HIGHRES_MAXORDER) {
+            if (! (order in this._highResIndexOrder3[ipixOrder3])) {
+                this._highResIndexOrder3[ipixOrder3][order] = [];
+            }
+            this._highResIndexOrder3[ipixOrder3][order].push(ipix);
+
+            var degradedOrder = MOC.LOWRES_MAXORDER;
+            var degradedIpix  = Math.floor(ipix / Math.pow(4, (order - degradedOrder)));
+            var degradedIpixOrder3 = Math.floor( degradedIpix * Math.pow(4, (3 - degradedOrder)) );
+            if (! (degradedOrder in this._lowResIndexOrder3[degradedIpixOrder3])) {
+                this._lowResIndexOrder3[degradedIpixOrder3][degradedOrder]= [];
+            }
+            this._lowResIndexOrder3[degradedIpixOrder3][degradedOrder].push(degradedIpix);
+        }
+        // 3. if order > HIGHRES_MAXORDER , degrade ipix for low res and high res cells
+        else {
+            // low res cells
+            var degradedOrder = MOC.LOWRES_MAXORDER;
+            var degradedIpix  = Math.floor(ipix / Math.pow(4, (order - degradedOrder)));
+            var degradedIpixOrder3 = Math.floor(degradedIpix * Math.pow(4, (3 - degradedOrder)) );
+            if (! (degradedOrder in this._lowResIndexOrder3[degradedIpixOrder3])) {
+                this._lowResIndexOrder3[degradedIpixOrder3][degradedOrder]= [];
+            }
+            this._lowResIndexOrder3[degradedIpixOrder3][degradedOrder].push(degradedIpix);
+
+
+            // high res cells
+            degradedOrder = MOC.HIGHRES_MAXORDER;
+            degradedIpix  = Math.floor(ipix / Math.pow(4, (order - degradedOrder)));
+            var degradedIpixOrder3 = Math.floor(degradedIpix * Math.pow(4, (3 - degradedOrder)) );
+            if (! (degradedOrder in this._highResIndexOrder3[degradedIpixOrder3])) {
+                this._highResIndexOrder3[degradedIpixOrder3][degradedOrder]= [];
+            }
+            this._highResIndexOrder3[degradedIpixOrder3][degradedOrder].push(degradedIpix);
+        }
+
+        this.nbCellsDeepestLevel += Math.pow(4, (this.order - order));
+    };
+
+
+    /**
+     *  Return a value between 0 and 1 denoting the fraction of the sky
+     *  covered by the MOC
+     */
+    MOC.prototype.skyFraction = function() {
+        return this.nbCellsDeepestLevel / (12 * Math.pow(4, this.order));
+    };
+
+    /**
+     * set MOC data by parsing a MOC serialized in JSON
+     * (as defined in IVOA MOC document, section 3.1.1)
+     */
+    MOC.prototype.dataFromJSON = function(jsonMOC) {
+        var order, ipix;
+        // 1. Compute the order (order of the deepest cells contained in the moc)
+        for (var orderStr in jsonMOC) {
+            if (jsonMOC.hasOwnProperty(orderStr)) {
+                order = parseInt(orderStr);
+                if (this.order===undefined || order > this.order) {
+                    this.order = order;
+                }
+            }
+        }
+
+        // 2. Build the mocs (LOW and HIGH res ones)
+        for (var orderStr in jsonMOC) {
+            if (jsonMOC.hasOwnProperty(orderStr)) {
+                order = parseInt(orderStr);
+                for (var k=0; k<jsonMOC[orderStr].length; k++) {
+                    ipix = jsonMOC[orderStr][k];
+                    this._addPix(order, ipix);
+                }
+            }
+        }
+
+        this.reportChange();
+        this.ready = true;
+    };
+
+    /**
+     * set MOC data by parsing a URL pointing to a FITS MOC file
+     */
+    MOC.prototype.dataFromFITSURL = function(mocURL, successCallback) {
+        var self = this;
+        var callback = function() {
+            // note: in the callback, 'this' refers to the FITS instance
+
+            // first, let's find MOC norder
+            var hdr0;
+            try {
+                // A zero-length hdus array might mean the served URL does not have CORS header
+                // --> let's try again through the proxy
+                if (this.hdus.length == 0) {
+                    if (self.proxyCalled !== true) {
+                        self.proxyCalled = true;
+                        var proxiedURL = Aladin.JSONP_PROXY + '?url=' + encodeURIComponent(self.dataURL);
+                        new astro.FITS(proxiedURL, callback);
+                    }
+
+                    return;
+                }
+                hdr0 = this.getHeader(0);
+            }
+            catch (e) {
+                console.error('Could not get header of extension #0');
+                return;
+            }
+            var hdr1 = this.getHeader(1);
+
+            if (hdr0.contains('HPXMOC')) {
+                self.order = hdr0.get('HPXMOC')
+            }
+            else if (hdr0.contains('MOCORDER')) {
+                self.order = hdr0.get('MOCORDER')
+            }
+            else if (hdr1.contains('HPXMOC')) {
+                self.order = hdr1.get('HPXMOC')
+            }
+            else if (hdr1.contains('MOCORDER')) {
+                self.order = hdr1.get('MOCORDER')
+            }
+            else {
+                console.error('Can not find MOC order in FITS file');
+                return;
+            }
+
+
+            var data = this.getDataUnit(1);
+            var colName = data.columns[0];
+            data.getRows(0, data.rows, function(rows) {
+                for (var k=0; k<rows.length; k++) {
+                    var uniq = rows[k][colName];
+                    var order = Math.floor(Math.floor(log2(Math.floor(uniq/4))) / 2);
+                    var ipix = uniq - 4 *(Math.pow(4, order));
+
+
+
+                    self._addPix(order, ipix);
+                }
+
+            });
+            data = null; // this helps releasing memory
+
+            self._removeDuplicatesFromIndexes();
+
+            if (successCallback) {
+                successCallback();
+            }
+
+            self.reportChange();
+            self.ready = true;
+        }; // end of callback function
+
+        this.dataURL = mocURL;
+
+        // instantiate the FITS object which will fetch the URL passed as parameter
+        new astro.FITS(this.dataURL, callback);
+    };
+
+    MOC.prototype.setView = function(view) {
+        this.view = view;
+        this.reportChange();
+    };
+
+    MOC.prototype.draw = function(ctx, projection, viewFrame, width, height, largestDim, zoomFactor, fov) {
+        if (! this.isShowing || ! this.ready) {
+            return;
+        }
+
+        var mocCells = fov > MOC.PIVOT_FOV && this.adaptativeDisplay ? this._lowResIndexOrder3 : this._highResIndexOrder3;
+
+        this._drawCells(ctx, mocCells, fov, projection, viewFrame, CooFrameEnum.J2000, width, height, largestDim, zoomFactor);
+    };
+
+    MOC.prototype._drawCells = function(ctx, mocCellsIdxOrder3, fov, projection, viewFrame, surveyFrame, width, height, largestDim, zoomFactor) {
+        ctx.lineWidth = this.lineWidth;
+        // if opacity==1, we draw solid lines, else we fill each HEALPix cell
+        if (this.opacity==1) {
+            ctx.strokeStyle = this.color;
+        }
+        else {
+            ctx.fillStyle = this.color;
+            ctx.globalAlpha = this.opacity;
+        }
+
+
+        ctx.beginPath();
+
+        var orderedKeys = [];
+        for (var k=0; k<768; k++) {
+            var mocCells = mocCellsIdxOrder3[k];
+            for (key in mocCells) {
+                orderedKeys.push(parseInt(key));
+            }
+        }
+        orderedKeys.sort(function(a, b) {return a - b;});
+        var norderMax = orderedKeys[orderedKeys.length-1];
+
+        var nside, xyCorners, ipix;
+        var potentialVisibleHpxCellsOrder3 = this.view.getVisiblePixList(3, CooFrameEnum.J2000);
+        var visibleHpxCellsOrder3 = [];
+        // let's test first all potential visible cells and keep only the one with a projection inside the view
+        for (var k=0; k<potentialVisibleHpxCellsOrder3.length; k++) {
+            var ipix = potentialVisibleHpxCellsOrder3[k];
+            xyCorners = getXYCorners(8, ipix, viewFrame, surveyFrame, width, height, largestDim, zoomFactor, projection);
+            if (xyCorners) {
+                visibleHpxCellsOrder3.push(ipix);
+            }
+        }
+
+        var counter = 0;
+        var mocCells;
+        for (var norder=0; norder<=norderMax; norder++) {
+            nside = 1 << norder;
+
+            for (var i=0; i<visibleHpxCellsOrder3.length; i++) {
+                var ipixOrder3 = visibleHpxCellsOrder3[i];
+                mocCells = mocCellsIdxOrder3[ipixOrder3];
+                if (typeof mocCells[norder]==='undefined') {
+                    continue;
+                }
+
+                if (norder<=3) {
+                    for (var j=0; j<mocCells[norder].length; j++) {
+                        ipix = mocCells[norder][j];
+                        var factor = Math.pow(4, (3-norder));
+                        var startIpix = ipix * factor;
+                        for (var k=0; k<factor; k++) {
+                            norder3Ipix = startIpix + k;
+                            xyCorners = getXYCorners(8, norder3Ipix, viewFrame, surveyFrame, width, height, largestDim, zoomFactor, projection);
+                            if (xyCorners) {
+                                drawCorners(ctx, xyCorners);
+                            }
+                        }
+                    }
+                }
+                else {
+                    for (var j=0; j<mocCells[norder].length; j++) {
+                        ipix = mocCells[norder][j];
+                        var parentIpixOrder3 = Math.floor(ipix/Math.pow(4, norder-3));
+                        xyCorners = getXYCorners(nside, ipix, viewFrame, surveyFrame, width, height, largestDim, zoomFactor, projection);
+                        if (xyCorners) {
+                            drawCorners(ctx, xyCorners);
+                        }
+                    }
+                }
+            }
+        }
+
+
+        if (this.opacity==1) {
+            ctx.stroke();
+        }
+        else {
+            ctx.fill();
+            ctx.globalAlpha = 1.0;
+        }
+    };
+
+    var drawCorners = function(ctx, xyCorners) {
+        ctx.moveTo(xyCorners[0].vx, xyCorners[0].vy);
+        ctx.lineTo(xyCorners[1].vx, xyCorners[1].vy);
+        ctx.lineTo(xyCorners[2].vx, xyCorners[2].vy);
+        ctx.lineTo(xyCorners[3].vx, xyCorners[3].vy);
+        ctx.lineTo(xyCorners[0].vx, xyCorners[0].vy);
+    }
+
+    // remove duplicate items from array a
+    var uniq = function(a) {
+        var seen = {};
+        var out = [];
+        var len = a.length;
+        var j = 0;
+        for (var i = 0; i < len; i++) {
+            var item = a[i];
+            if (seen[item] !== 1) {
+                seen[item] = 1;
+                out[j++] = item;
+            }
+        }
+
+        return out;
+    };
+
+
+    // TODO: merge with what is done in View.getVisibleCells
+    var _spVec = new SpatialVector();
+    var getXYCorners = function(nside, ipix, viewFrame, surveyFrame, width, height, largestDim, zoomFactor, projection) {
+        var cornersXYView = [];
+        var cornersXY = [];
+
+        var spVec = _spVec;
+
+        var corners = HealpixCache.corners_nest(ipix, nside);
+        for (var k=0; k<4; k++) {
+            spVec.setXYZ(corners[k].x, corners[k].y, corners[k].z);
+
+            // need for frame transformation ?
+            if (surveyFrame && surveyFrame.system != viewFrame.system) {
+                if (surveyFrame.system == CooFrameEnum.SYSTEMS.J2000) {
+                    var radec = CooConversion.J2000ToGalactic([spVec.ra(), spVec.dec()]);
+                    lon = radec[0];
+                    lat = radec[1];
+                }
+                else if (surveyFrame.system == CooFrameEnum.SYSTEMS.GAL) {
+                    var radec = CooConversion.GalacticToJ2000([spVec.ra(), spVec.dec()]);
+                    lon = radec[0];
+                    lat = radec[1];
+                }
+            }
+            else {
+                lon = spVec.ra();
+                lat = spVec.dec();
+            }
+
+            cornersXY[k] = projection.project(lon, lat);
+        }
+
+
+        if (cornersXY[0] == null ||  cornersXY[1] == null  ||  cornersXY[2] == null ||  cornersXY[3] == null ) {
+            return null;
+        }
+
+        for (var k=0; k<4; k++) {
+            cornersXYView[k] = AladinUtils.xyToView(cornersXY[k].X, cornersXY[k].Y, width, height, largestDim, zoomFactor);
+        }
+
+        var indulge = 10;
+        // detect pixels outside view. Could be improved !
+        // we minimize here the number of cells returned
+        if( cornersXYView[0].vx<0 && cornersXYView[1].vx<0 && cornersXYView[2].vx<0 &&cornersXYView[3].vx<0) {
+            return null;
+        }
+        if( cornersXYView[0].vy<0 && cornersXYView[1].vy<0 && cornersXYView[2].vy<0 &&cornersXYView[3].vy<0) {
+            return null;
+        }
+        if( cornersXYView[0].vx>=width && cornersXYView[1].vx>=width && cornersXYView[2].vx>=width &&cornersXYView[3].vx>=width) {
+            return null;
+        }
+        if( cornersXYView[0].vy>=height && cornersXYView[1].vy>=height && cornersXYView[2].vy>=height &&cornersXYView[3].vy>=height) {
+            return null;
+        }
+
+        cornersXYView = AladinUtils.grow2(cornersXYView, 1);
+        return cornersXYView;
+    };
+
+    MOC.prototype.reportChange = function() {
+        this.view && this.view.requestRedraw();
+    };
+
+    MOC.prototype.show = function() {
+        if (this.isShowing) {
+            return;
+        }
+        this.isShowing = true;
+        this.reportChange();
+    };
+
+    MOC.prototype.hide = function() {
+        if (! this.isShowing) {
+            return;
+        }
+        this.isShowing = false;
+        this.reportChange();
+    };
+
+    // Tests whether a given (ra, dec) point on the sky is within the current MOC object
+    //
+    // returns true if point is contained, false otherwise
+    MOC.prototype.contains = function(ra, dec) {
+        var hpxIdx = new HealpixIndex(Math.pow(2, this.order));
+        hpxIdx.init();
+        var polar = Utils.radecToPolar(ra, dec);
+        var ipix = hpxIdx.ang2pix_nest(polar.theta, polar.phi);
+        var ipixMapByOrder = {};
+        for (var curOrder=0; curOrder<=this.order; curOrder++) {
+            ipixMapByOrder[curOrder] = Math.floor(ipix / Math.pow(4, this.order - curOrder));
+        }
+
+        // first look for large HEALPix cells (order<3)
+        for (var ipixOrder3=0; ipixOrder3<768; ipixOrder3++) {
+            var mocCells = this._highResIndexOrder3[ipixOrder3];
+            for (var order in mocCells) {
+                if (order<3) {
+                    for (var k=mocCells[order].length; k>=0; k--) {
+                        if (ipixMapByOrder[order] == mocCells[order][k]) {
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+
+        // look for finer cells
+        var ipixOrder3 = ipixMapByOrder[3];
+        var mocCells = this._highResIndexOrder3[ipixOrder3];
+        for (var order in mocCells) {
+            for (var k=mocCells[order].length; k>=0; k--) {
+                if (ipixMapByOrder[order] == mocCells[order][k]) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    };
+
+
+
+    return MOC;
+
+})();
+
+
+// Copyright 2015 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File CooGrid
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+CooGrid = (function() {
+    var CooGrid = function() {
+    };
+
+    function viewxy2lonlat(projection, vx, vy, width, height, largestDim, zoomFactor) {
+        var xy = AladinUtils.viewToXy(vx, vy, width, height, largestDim, zoomFactor);
+        var lonlat;
+        try {
+            lonlat = projection.unproject(xy.x, xy.y);
+        }
+        catch(err) {
+            return null;
+        }
+        return {lon: lonlat.ra, lat: lonlat.dec};
+    };
+
+    var NB_STEPS = 10;
+    var NB_LINES = 10;
+
+    CooGrid.prototype.redraw = function(ctx, projection, frame, width, height, largestDim, zoomFactor, fov) {
+        if (fov>60) { // currently not supported
+            return;
+        }
+
+        var lonMax = 0, lonMin = 359.9999, latMax = -90, latMin = 90;
+        var lonlat1 = viewxy2lonlat(projection, 0, 0, width, height, largestDim, zoomFactor);
+        var lonlat2 = viewxy2lonlat(projection, width-1, height-1, width, height, largestDim, zoomFactor);
+        lonMin = Math.min(lonlat1.lon, lonlat2.lon);
+        lonMax = Math.max(lonlat1.lon, lonlat2.lon);
+        latMin = Math.min(lonlat1.lat, lonlat2.lat);
+        latMax = Math.max(lonlat1.lat, lonlat2.lat);
+
+        var lonlat3 = viewxy2lonlat(projection, 0, height-1, width, height, largestDim, zoomFactor);
+        lonMin = Math.min(lonMin, lonlat3.lon);
+        lonMax = Math.max(lonMax, lonlat3.lon);
+        latMin = Math.min(latMin, lonlat3.lat);
+        latMax = Math.max(latMax, lonlat3.lat);
+
+        var lonlat4 = viewxy2lonlat(projection, width-1, 0, width, height, largestDim, zoomFactor);
+        lonMin = Math.min(lonMin, lonlat4.lon);
+        lonMax = Math.max(lonMax, lonlat4.lon);
+        latMin = Math.min(latMin, lonlat4.lat);
+        latMax = Math.max(latMax, lonlat4.lat);
+
+
+
+        var lonDiff = lonMax - lonMin;
+        var latDiff = latMax - latMin;
+
+        var LON_STEP, LAT_STEP;
+        if (fov>10) {
+            LON_STEP = 4;
+            LAT_STEP = 4;
+        }
+        else if (fov>1) {
+            LON_STEP = 1;
+            LAT_STEP = 1;
+        }
+        else if (fov>0.1) {
+            LON_STEP = 0.1;
+            LAT_STEP = 0.1;
+        }
+        else {
+            LON_STEP = 0.01;
+            LAT_STEP = 0.01;
+        }
+
+        var lonStart = Math.round(lonMin % LON_STEP) * (LON_STEP);
+        var latStart = Math.round(latMin % LAT_STEP) * (LAT_STEP);
+
+
+
+        ctx.lineWidth = 1;
+        ctx.strokeStyle = "rgb(120,120,255)";
+        // draw iso-latitudes lines
+        for (var lat=latStart; lat<latMax+LAT_STEP; lat+=LAT_STEP) {
+            ctx.beginPath();
+
+            var vxy;
+            vxy = AladinUtils.radecToViewXy(lonMin, lat, projection, CooFrameEnum.J2000, width, height, largestDim, zoomFactor);
+            if (!vxy) {
+                continue;
+            }
+            ctx.moveTo(vxy.vx, vxy.vy);
+            var k = 0;
+            for (var lon=lonMin; lon<lonMax+LON_STEP; lon+=lonDiff/10) {
+                k++;
+                vxy = AladinUtils.radecToViewXy(lon, lat, projection, CooFrameEnum.J2000, width, height, largestDim, zoomFactor);
+                ctx.lineTo(vxy.vx, vxy.vy);
+                if (k==3 ) {
+                    ctx.strokeText(lat.toFixed(2), vxy.vx, vxy.vy-2);
+                }
+
+            }
+            ctx.stroke();
+        }
+
+        for (var lon=lonStart; lon<lonMax+LON_STEP; lon+=LON_STEP) {
+            ctx.beginPath();
+
+            var vxy;
+            vxy = AladinUtils.radecToViewXy(lon, latMin, projection, CooFrameEnum.J2000, width, height, largestDim, zoomFactor);
+            if (!vxy) {
+                continue;
+            }
+            ctx.moveTo(vxy.vx, vxy.vy);
+            var k = 0;
+            for (var lat=latMin; lat<latMax+LAT_STEP; lat+=latDiff/10) {
+                k++;
+                vxy = AladinUtils.radecToViewXy(lon, lat, projection, CooFrameEnum.J2000, width, height, largestDim, zoomFactor);
+                ctx.lineTo(vxy.vx, vxy.vy);
+                if (k==3 ) {
+                    ctx.strokeText(lon.toFixed(2), vxy.vx, vxy.vy-2);
+                }
+            }
+            ctx.stroke();
+        }
+
+
+
+    };
+
+
+
+    return CooGrid;
+})();
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File Footprint
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+Footprint = (function() {
+    // constructor
+    Footprint = function(polygons) {
+        this.polygons = polygons;
+    	this.overlay = null;
+
+        // TODO : all graphic overlays should have an id
+        this.id = 'footprint-' + Utils.uuidv4();
+
+    	this.isShowing = true;
+    	this.isSelected = false;
+    };
+
+    Footprint.prototype.setOverlay = function(overlay) {
+        this.overlay = overlay;
+    };
+
+    Footprint.prototype.show = function() {
+        if (this.isShowing) {
+            return;
+        }
+        this.isShowing = true;
+        if (this.overlay) {
+            this.overlay.reportChange();
+        }
+    };
+
+    Footprint.prototype.hide = function() {
+        if (! this.isShowing) {
+            return;
+        }
+        this.isShowing = false;
+        if (this.overlay) {
+            this.overlay.reportChange();
+        }
+    };
+
+    Footprint.prototype.dispatchClickEvent = function() {
+        if (this.overlay) {
+            // footprint selection code adapted from Fabrizio Giordano dev. from Serco for ESA/ESDC
+            //window.dispatchEvent(new CustomEvent("footprintClicked", {
+            this.overlay.view.aladinDiv.dispatchEvent(new CustomEvent("footprintClicked", {
+                detail: {
+                    footprintId: this.id,
+                    overlayName: this.overlay.name
+                }
+            }));
+        }
+    };
+
+    Footprint.prototype.select = function() {
+        if (this.isSelected) {
+            return;
+        }
+        this.isSelected = true;
+        if (this.overlay) {
+/*
+            // footprint selection code adapted from Fabrizio Giordano dev. from Serco for ESA/ESDC
+            //window.dispatchEvent(new CustomEvent("footprintClicked", {
+            this.overlay.view.aladinDiv.dispatchEvent(new CustomEvent("footprintClicked", {
+                detail: {
+                    footprintId: this.id,
+                    overlayName: this.overlay.name
+                }
+            }));
+*/
+
+            this.overlay.reportChange();
+        }
+    };
+
+    Footprint.prototype.deselect = function() {
+        if (! this.isSelected) {
+            return;
+        }
+        this.isSelected = false;
+        if (this.overlay) {
+            this.overlay.reportChange();
+        }
+    };
+
+    return Footprint;
+})();
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File Popup.js
+ *
+ * Author: Thomas Boch [CDS]
+ *
+ *****************************************************************************/
+
+Popup = (function() {
+
+
+    // constructor
+    Popup = function(parentDiv, view) {
+        this.domEl = $('<div class="aladin-popup-container"><div class="aladin-popup"><a class="aladin-closeBtn">&times;</a><div class="aladin-popupTitle"></div><div class="aladin-popupText"></div></div><div class="aladin-popup-arrow"></div></div>');
+        this.domEl.appendTo(parentDiv);
+
+        this.view = view;
+
+
+        var self = this;
+        // close popup
+        this.domEl.find('.aladin-closeBtn').click(function() {self.hide();});
+
+    };
+
+    Popup.prototype.hide = function() {
+        this.domEl.hide();
+
+        this.view.mustClearCatalog=true;
+        this.view.catalogForPopup.hide();
+    };
+
+    Popup.prototype.show = function() {
+        this.domEl.show();
+    };
+
+    Popup.prototype.setTitle = function(title) {
+        this.domEl.find('.aladin-popupTitle').html(title || '');
+    };
+
+    Popup.prototype.setText = function(text) {
+        this.domEl.find('.aladin-popupText').html(text || '');
+        this.w = this.domEl.outerWidth();
+        this.h = this.domEl.outerHeight();
+    };
+
+    Popup.prototype.setSource = function(source) {
+        // remove reference to popup for previous source
+        if (this.source) {
+            this.source.popup = null;
+        }
+        source.popup = this;
+        this.source = source;
+        this.setPosition(source.x, source.y);
+    };
+
+    Popup.prototype.setPosition = function(x, y) {
+        var newX = x - this.w/2;
+        var newY = y - this.h;
+        if (this.source) {
+            newY += this.source.catalog.sourceSize/2;
+        }
+
+        this.domEl[0].style.left = newX + 'px';
+        this.domEl[0].style.top  = newY + 'px';
+    };
+
+    return Popup;
+})();
+
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File Circle
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+// TODO : Circle and Footprint should inherit from the same root object
+Circle = (function() {
+    // constructor
+    Circle = function(centerRaDec, radiusDegrees, options) {
+        options = options || {};
+
+        this.color = options['color'] || undefined;
+
+        // TODO : all graphic overlays should have an id
+        this.id = 'circle-' + Utils.uuidv4();
+
+        this.setCenter(centerRaDec);
+        this.setRadius(radiusDegrees);
+    	this.overlay = null;
+
+    	this.isShowing = true;
+    	this.isSelected = false;
+    };
+
+    Circle.prototype.setOverlay = function(overlay) {
+        this.overlay = overlay;
+    };
+
+    Circle.prototype.show = function() {
+        if (this.isShowing) {
+            return;
+        }
+        this.isShowing = true;
+        if (this.overlay) {
+            this.overlay.reportChange();
+        }
+    };
+
+    Circle.prototype.hide = function() {
+        if (! this.isShowing) {
+            return;
+        }
+        this.isShowing = false;
+        if (this.overlay) {
+            this.overlay.reportChange();
+        }
+    };
+
+    Circle.prototype.dispatchClickEvent = function() {
+        if (this.overlay) {
+            // footprint selection code adapted from Fabrizio Giordano dev. from Serco for ESA/ESDC
+            //window.dispatchEvent(new CustomEvent("footprintClicked", {
+            this.overlay.view.aladinDiv.dispatchEvent(new CustomEvent("footprintClicked", {
+                detail: {
+                    footprintId: this.id,
+                    overlayName: this.overlay.name
+                }
+            }));
+        }
+    };
+
+    Circle.prototype.select = function() {
+        if (this.isSelected) {
+            return;
+        }
+        this.isSelected = true;
+        if (this.overlay) {
+/*
+            this.overlay.view.aladinDiv.dispatchEvent(new CustomEvent("footprintClicked", {
+                detail: {
+                    footprintId: this.id,
+                    overlayName: this.overlay.name
+                }
+            }));
+*/
+
+            this.overlay.reportChange();
+        }
+    };
+
+    Circle.prototype.deselect = function() {
+        if (! this.isSelected) {
+            return;
+        }
+        this.isSelected = false;
+        if (this.overlay) {
+            this.overlay.reportChange();
+        }
+    };
+
+
+
+    Circle.prototype.setCenter = function(centerRaDec) {
+        this.centerRaDec = centerRaDec;
+        if (this.overlay) {
+            this.overlay.reportChange();
+        }
+    };
+
+    Circle.prototype.setRadius = function(radiusDegrees) {
+        this.radiusDegrees = radiusDegrees;
+        if (this.overlay) {
+            this.overlay.reportChange();
+        }
+    };
+
+    // TODO
+    Circle.prototype.draw = function(ctx, projection, frame, width, height, largestDim, zoomFactor, noStroke) {
+        if (! this.isShowing) {
+            return;
+        }
+
+
+        noStroke = noStroke===true || false;
+
+        var centerXy;
+        if (frame.system != CooFrameEnum.SYSTEMS.J2000) {
+            var lonlat = CooConversion.J2000ToGalactic([this.centerRaDec[0], this.centerRaDec[1]]);
+            centerXy = projection.project(lonlat[0], lonlat[1]);
+        }
+        else {
+            centerXy = projection.project(this.centerRaDec[0], this.centerRaDec[1]);
+        }
+        if (!centerXy) {
+            return;
+        }
+        var centerXyview = AladinUtils.xyToView(centerXy.X, centerXy.Y, width, height, largestDim, zoomFactor, false);
+
+        // compute value of radius in pixels in current projection
+        var circlePtXy;
+        var ra = this.centerRaDec[0];
+        var dec = this.centerRaDec[1] + (ra>0 ? - this.radiusDegrees : this.radiusDegrees);
+        if (frame.system != CooFrameEnum.SYSTEMS.J2000) {
+            var lonlat = CooConversion.J2000ToGalactic([ra, dec]);
+            circlePtXy = projection.project(lonlat[0], lonlat[1]);
+        }
+        else {
+            circlePtXy = projection.project(ra, dec);
+        }
+        if (!circlePtXy) {
+            return;
+        }
+        var circlePtXyView = AladinUtils.xyToView(circlePtXy.X, circlePtXy.Y, width, height, largestDim, zoomFactor, false);
+        var dx = circlePtXyView.vx - centerXyview.vx;
+        var dy = circlePtXyView.vy - centerXyview.vy;
+        var radiusInPix = Math.sqrt(dx*dx + dy*dy);
+
+        // TODO : check each 4 point until show
+        var baseColor = this.color;
+        if (! baseColor && this.overlay) {
+            baseColor = this.overlay.color;
+        }
+        if (! baseColor) {
+            baseColor = '#ff0000';
+        }
+
+        if (this.isSelected) {
+            ctx.strokeStyle= Overlay.increaseBrightness(baseColor, 50);
+        }
+        else {
+            ctx.strokeStyle= baseColor;
+        }
+
+        ctx.beginPath();
+        ctx.arc(centerXyview.vx, centerXyview.vy, radiusInPix, 0, 2*Math.PI, false);
+        if (!noStroke) {
+            ctx.stroke();
+        }
+    };
+
+    return Circle;
+})();
+// Copyright 2015 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * Class Polyline
+ *
+ * A Polyline is a graphical overlay made of several connected points
+ *
+ * TODO: Polyline and Circle should derive from a common base class
+ * TODO: index polyline, Circle in HEALPix pixels to avoid unneeded calls to draw
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+Polyline= (function() {
+    // constructor
+    Polyline = function(radecArray, options) {
+        options = options || {};
+        this.color = options['color'] || "white";
+        this.lineWidth = options["lineWidth"] || 2;
+
+        this.radecArray = radecArray;
+        this.overlay = null;
+
+    	this.isShowing = true;
+    	this.isSelected = false;
+    };
+
+    Polyline.prototype.setOverlay = function(overlay) {
+        this.overlay = overlay;
+    };
+
+    Polyline.prototype.show = function() {
+        if (this.isShowing) {
+            return;
+        }
+        this.isShowing = true;
+        if (this.overlay) {
+            this.overlay.reportChange();
+        }
+    };
+
+    Polyline.prototype.hide = function() {
+        if (! this.isShowing) {
+            return;
+        }
+        this.isShowing = false;
+        if (this.overlay) {
+            this.overlay.reportChange();
+        }
+    };
+
+    Polyline.prototype.select = function() {
+        if (this.isSelected) {
+            return;
+        }
+        this.isSelected = true;
+        if (this.overlay) {
+            this.overlay.reportChange();
+        }
+    };
+
+    Polyline.prototype.deselect = function() {
+        if (! this.isSelected) {
+            return;
+        }
+        this.isSelected = false;
+        if (this.overlay) {
+            this.overlay.reportChange();
+        }
+    };
+
+    Polyline.prototype.setLineWidth = function(lineWidth) {
+        if (this.lineWidth == lineWidth) {
+            return;
+        }
+        this.lineWidth = lineWidth;
+        this.overlay.reportChange();
+    };
+
+    Polyline.prototype.setColor = function(color) {
+        if (this.color == color) {
+            return;
+        }
+        this.color = color;
+        this.overlay.reportChange();
+    };
+
+    Polyline.prototype.draw = function(ctx, projection, frame, width, height, largestDim, zoomFactor) {
+        if (! this.isShowing) {
+            return;
+        }
+
+        if (! this.radecArray || this.radecArray.length<2) {
+            return;
+        }
+
+        if (this.color) {
+            ctx.strokeStyle= this.color;
+        }
+        var start = AladinUtils.radecToViewXy(this.radecArray[0][0], this.radecArray[0][1], projection, frame, width, height, largestDim, zoomFactor);
+
+        for (var k = 0; k < this.radecArray.length; k++) {
+            start = AladinUtils.radecToViewXy(this.radecArray[k][0], this.radecArray[k][1], projection, frame, width, height, largestDim, zoomFactor);
+            if (start) {
+                break;
+            }
+        }
+        if (!start) {
+            return;
+        }
+
+        ctx.moveTo(start.vx, start.vy);
+        var pt;
+        var newSeg = false;
+        var drawingNewSeg = true;
+        for (var k = 1; k < this.radecArray.length; k++) {
+            pt = AladinUtils.radecToViewXy(this.radecArray[k][0], this.radecArray[k][1], projection, frame, width, height, largestDim, zoomFactor);
+            if (!pt) {
+                if (drawingNewSeg) {
+                    //console.log("closing segment");
+                    ctx.stroke();
+                }
+                drawingNewSeg = false;
+                newSeg = true;
+            } else {
+                if (newSeg) {
+                    //console.log ("starting newSeg at "+pt.vx+" "+pt.vy);
+                    drawingNewSeg = true;
+                    ctx.beginPath();
+                    ctx.lineWidth = this.lineWidth;
+                    ctx.moveTo(pt.vx, pt.vy);
+                    newSeg = false;
+                } else {
+                    ctx.lineTo(pt.vx, pt.vy);
+                }
+            }
+
+        }
+        ctx.stroke();
+    };
+
+    return Polyline;
+})();
+// Copyright 2015 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File Overlay
+ *
+ * Description: a plane holding overlays (footprints, polylines, circles)
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+Overlay = (function() {
+   Overlay = function(options) {
+        options = options || {};
+
+        this.type = 'overlay';
+
+    	this.name = options.name || "overlay";
+    	this.color = options.color || Color.getNextColor();
+
+    	this.lineWidth = options["lineWidth"] || 2;
+
+    	//this.indexationNorder = 5; // at which level should we index overlays?
+    	this.overlays = [];
+    	this.overlay_items = []; // currently Circle or Polyline
+    	//this.hpxIdx = new HealpixIndex(this.indexationNorder);
+    	//this.hpxIdx.init();
+
+    	this.isShowing = true;
+    };
+
+
+    // TODO : show/hide methods should be integrated in a parent class
+    Overlay.prototype.show = function() {
+        if (this.isShowing) {
+            return;
+        }
+        this.isShowing = true;
+        this.reportChange();
+    };
+
+    Overlay.prototype.hide = function() {
+        if (! this.isShowing) {
+            return;
+        }
+        this.isShowing = false;
+        this.reportChange();
+    };
+
+    // return an array of Footprint from a STC-S string
+    Overlay.parseSTCS = function(stcs) {
+        var footprints = [];
+        var parts = stcs.match(/\S+/g);
+        var k = 0, len = parts.length;
+        while(k<len) {
+            var s = parts[k].toLowerCase();
+            if(s=='polygon') {
+                var curPolygon = [];
+                k++;
+                frame = parts[k].toLowerCase();
+                if (frame=='icrs' || frame=='j2000' || frame=='fk5') {
+                    while(k+2<len) {
+                        var ra = parseFloat(parts[k+1]);
+                        if (isNaN(ra)) {
+                            break;
+                        }
+                        var dec = parseFloat(parts[k+2]);
+                        curPolygon.push([ra, dec]);
+                        k += 2;
+                    }
+                    curPolygon.push(curPolygon[0]);
+                    footprints.push(new Footprint(curPolygon));
+                }
+            }
+            else if (s=='circle') {
+                var frame;
+                k++;
+                frame = parts[k].toLowerCase();
+
+                if (frame=='icrs' || frame=='j2000' || frame=='fk5') {
+                    var ra, dec, radiusDegrees;
+
+                    ra = parseFloat(parts[k+1]);
+                    dec = parseFloat(parts[k+2]);
+                    radiusDegrees = parseFloat(parts[k+3]);
+
+                    footprints.push(A.circle(ra, dec, radiusDegrees));
+
+                    k += 3;
+                }
+            }
+
+            k++;
+        }
+
+        return footprints;
+    };
+
+    // ajout d'un tableau d'overlays (= objets Footprint, Circle ou Polyline)
+    Overlay.prototype.addFootprints = function(overlaysToAdd) {
+    	for (var k=0, len=overlaysToAdd.length; k<len; k++) {
+            this.add(overlaysToAdd[k], false);
+        }
+
+        this.view.requestRedraw();
+    };
+
+    // TODO : item doit pouvoir prendre n'importe quoi en param (footprint, circle, polyline)
+    Overlay.prototype.add = function(item, requestRedraw) {
+        requestRedraw = requestRedraw !== undefined ? requestRedraw : true;
+
+        if (item instanceof Footprint) {
+            this.overlays.push(item);
+        }
+        else {
+            this.overlay_items.push(item);
+        }
+        item.setOverlay(this);
+
+        if (requestRedraw) {
+            this.view.requestRedraw();
+        }
+    };
+
+
+    // return a footprint by index
+   Overlay.prototype.getFootprint = function(idx) {
+        if (idx<this.footprints.length) {
+            return this.footprints[idx];
+        }
+        else {
+            return null;
+        }
+    };
+
+    Overlay.prototype.setView = function(view) {
+        this.view = view;
+    };
+
+    Overlay.prototype.removeAll = function() {
+        // TODO : RAZ de l'index
+        this.overlays = [];
+        this.overlay_items = [];
+    };
+
+    Overlay.prototype.draw = function(ctx, projection, frame, width, height, largestDim, zoomFactor) {
+        if (!this.isShowing) {
+            return;
+        }
+
+        // simple drawing
+        ctx.strokeStyle= this.color;
+
+        // 1. Drawing polygons
+
+        // TODO: les overlay polygons devrait se tracer lui meme (methode draw)
+        ctx.lineWidth = this.lineWidth;
+    	ctx.beginPath();
+    	xyviews = [];
+    	for (var k=0, len = this.overlays.length; k<len; k++) {
+    		xyviews.push(this.drawFootprint(this.overlays[k], ctx, projection, frame, width, height, largestDim, zoomFactor));
+    	}
+        ctx.stroke();
+
+    	// selection drawing
+        ctx.strokeStyle= Overlay.increaseBrightness(this.color, 50);
+        ctx.beginPath();
+        for (var k=0, len = this.overlays.length; k<len; k++) {
+            if (! this.overlays[k].isSelected) {
+                continue;
+            }
+            this.drawFootprintSelected(ctx, xyviews[k]);
+
+        }
+    	ctx.stroke();
+
+        // 2. Circle and polylines drawing
+    	for (var k=0; k<this.overlay_items.length; k++) {
+    	    this.overlay_items[k].draw(ctx, projection, frame, width, height, largestDim, zoomFactor);
+    	}
+    };
+
+    Overlay.increaseBrightness = function(hex, percent){
+        // strip the leading # if it's there
+        hex = hex.replace(/^\s*#|\s*$/g, '');
+
+        // convert 3 char codes --> 6, e.g. `E0F` --> `EE00FF`
+        if(hex.length == 3){
+            hex = hex.replace(/(.)/g, '$1$1');
+        }
+
+        var r = parseInt(hex.substr(0, 2), 16),
+            g = parseInt(hex.substr(2, 2), 16),
+            b = parseInt(hex.substr(4, 2), 16);
+
+        return '#' +
+                ((0|(1<<8) + r + (256 - r) * percent / 100).toString(16)).substr(1) +
+                ((0|(1<<8) + g + (256 - g) * percent / 100).toString(16)).substr(1) +
+                ((0|(1<<8) + b + (256 - b) * percent / 100).toString(16)).substr(1);
+    };
+
+
+    Overlay.prototype.drawFootprint = function(f, ctx, projection, frame, width, height, largestDim, zoomFactor) {
+        if (! f.isShowing) {
+            return null;
+        }
+        var xyviewArray = [];
+        var show = false;
+        var radecArray = f.polygons;
+        // for
+            for (var k=0, len=radecArray.length; k<len; k++) {
+                var xy;
+                if (frame.system != CooFrameEnum.SYSTEMS.J2000) {
+                    var lonlat = CooConversion.J2000ToGalactic([radecArray[k][0], radecArray[k][1]]);
+                    xy = projection.project(lonlat[0], lonlat[1]);
+                }
+                else {
+                    xy = projection.project(radecArray[k][0], radecArray[k][1]);
+                }
+                if (!xy) {
+                    return null;
+                }
+                var xyview = AladinUtils.xyToView(xy.X, xy.Y, width, height, largestDim, zoomFactor);
+                xyviewArray.push(xyview);
+                if (!show && xyview.vx<width  && xyview.vx>=0 && xyview.vy<=height && xyview.vy>=0) {
+                    show = true;
+                }
+            }
+
+            if (show) {
+                ctx.moveTo(xyviewArray[0].vx, xyviewArray[0].vy);
+                for (var k=1, len=xyviewArray.length; k<len; k++) {
+                    ctx.lineTo(xyviewArray[k].vx, xyviewArray[k].vy);
+                }
+            }
+            else {
+                //return null;
+            }
+        // end for
+
+        return xyviewArray;
+
+
+
+    };
+
+    Overlay.prototype.drawFootprintSelected = function(ctx, xyview) {
+        if (!xyview) {
+            return;
+        }
+
+        var xyviewArray = xyview;
+        ctx.moveTo(xyviewArray[0].vx, xyviewArray[0].vy);
+        for (var k=1, len=xyviewArray.length; k<len; k++) {
+            ctx.lineTo(xyviewArray[k].vx, xyviewArray[k].vy);
+        }
+    };
+
+
+
+    // callback function to be called when the status of one of the footprints has changed
+    Overlay.prototype.reportChange = function() {
+        this.view.requestRedraw();
+    };
+
+    return Overlay;
+})();
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File Source
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+cds.Source = (function() {
+    // constructor
+    cds.Source = function(ra, dec, data, options) {
+    	this.ra = ra;
+    	this.dec = dec;
+    	this.data = data;
+    	this.catalog = null;
+
+        this.marker = (options && options.marker) || false;
+        if (this.marker) {
+            this.popupTitle = (options && options.popupTitle) ? options.popupTitle : '';
+            this.popupDesc = (options && options.popupDesc) ? options.popupDesc : '';
+            this.useMarkerDefaultIcon = (options && options.useMarkerDefaultIcon!==undefined) ? options.useMarkerDefaultIcon : true;
+        }
+
+    	this.isShowing = true;
+    	this.isSelected = false;
+    };
+
+    cds.Source.prototype.setCatalog = function(catalog) {
+        this.catalog = catalog;
+    };
+
+    cds.Source.prototype.show = function() {
+        if (this.isShowing) {
+            return;
+        }
+        this.isShowing = true;
+        if (this.catalog) {
+            this.catalog.reportChange();
+        }
+    };
+
+    cds.Source.prototype.hide = function() {
+        if (! this.isShowing) {
+            return;
+        }
+        this.isShowing = false;
+        if (this.catalog) {
+            this.catalog.reportChange();
+        }
+    };
+
+    cds.Source.prototype.select = function() {
+        if (this.isSelected) {
+            return;
+        }
+        this.isSelected = true;
+        if (this.catalog) {
+            this.catalog.reportChange();
+        }
+    };
+
+    cds.Source.prototype.deselect = function() {
+        if (! this.isSelected) {
+            return;
+        }
+        this.isSelected = false;
+        if (this.catalog) {
+            this.catalog.reportChange();
+        }
+    };
+
+    // function called when a source is clicked. Called by the View object
+    cds.Source.prototype.actionClicked = function() {
+        if (this.catalog && this.catalog.onClick) {
+            var view = this.catalog.view;
+            if (this.catalog.onClick=='showTable') {
+                view.aladin.measurementTable.showMeasurement(this);
+                this.select();
+            }
+            else if (this.catalog.onClick=='showPopup') {
+                view.popup.setTitle('<br><br>');
+                var m = '<div class="aladin-marker-measurement">';
+                m += '<table>';
+                for (var key in this.data) {
+                    m += '<tr><td>' + key + '</td><td>' + this.data[key] + '</td></tr>';
+                }
+                m += '</table>';
+                m += '</div>';
+                view.popup.setText(m);
+                view.popup.setSource(this);
+                view.popup.show();
+            }
+            else if (typeof this.catalog.onClick === 'function') {
+                this.catalog.onClick(this);
+                view.lastClickedObject = this;
+            }
+
+        }
+    };
+
+
+    cds.Source.prototype.actionOtherObjectClicked = function() {
+        if (this.catalog && this.catalog.onClick) {
+            this.deselect();
+        }
+    };
+
+    return cds.Source;
+})();
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File Catalog
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+// TODO : harmoniser parsing avec classe ProgressiveCat
+cds.Catalog = (function() {
+   cds.Catalog = function(options) {
+        options = options || {};
+
+        this.type = 'catalog';    	this.name = options.name || "catalog";
+    	this.color = options.color || Color.getNextColor();
+    	this.sourceSize = options.sourceSize || 8;
+    	this.markerSize = options.sourceSize || 12;
+    	this.shape = options.shape || "square";
+        this.maxNbSources = options.limit || undefined;
+        this.onClick = options.onClick || undefined;
+
+        this.raField = options.raField || undefined; // ID or name of the field holding RA
+        this.decField = options.decField || undefined; // ID or name of the field holding dec
+
+    	this.indexationNorder = 5; // à quel niveau indexe-t-on les sources
+    	this.sources = [];
+    	this.hpxIdx = new HealpixIndex(this.indexationNorder);
+    	this.hpxIdx.init();
+
+        this.displayLabel = options.displayLabel || false;
+        this.labelColor = options.labelColor || this.color;
+        this.labelFont = options.labelFont || '10px sans-serif';
+        if (this.displayLabel) {
+            this.labelColumn = options.labelColumn;
+            if (!this.labelColumn) {
+                this.displayLabel = false;
+            }
+        }
+
+        if (this.shape instanceof Image || this.shape instanceof HTMLCanvasElement) {
+            this.sourceSize = this.shape.width;
+        }
+        this._shapeIsFunction = false; // if true, the shape is a function drawing on the canvas
+        if ($.isFunction(this.shape)) {
+            this._shapeIsFunction = true;
+        }
+
+    	this.selectionColor = '#00ff00';
+
+
+        // create this.cacheCanvas
+    	// cacheCanvas permet de ne créer le path de la source qu'une fois, et de le réutiliser (cf. http://simonsarris.com/blog/427-increasing-performance-by-caching-paths-on-canvas)
+        this.updateShape(options);
+
+        this.cacheMarkerCanvas = document.createElement('canvas');
+        this.cacheMarkerCanvas.width = this.markerSize;
+        this.cacheMarkerCanvas.height = this.markerSize;
+        var cacheMarkerCtx = this.cacheMarkerCanvas.getContext('2d');
+        cacheMarkerCtx.fillStyle = this.color;
+        cacheMarkerCtx.beginPath();
+        var half = (this.markerSize)/2.;
+        cacheMarkerCtx.arc(half, half, half-2, 0, 2 * Math.PI, false);
+        cacheMarkerCtx.fill();
+        cacheMarkerCtx.lineWidth = 2;
+        cacheMarkerCtx.strokeStyle = '#ccc';
+        cacheMarkerCtx.stroke();
+
+
+        this.isShowing = true;
+    };
+
+    cds.Catalog.createShape = function(shapeName, color, sourceSize) {
+        if (shapeName instanceof Image || shapeName instanceof HTMLCanvasElement) { // in this case, the shape is already created
+            return shapeName;
+        }
+        var c = document.createElement('canvas');
+        c.width = c.height = sourceSize;
+        var ctx= c.getContext('2d');
+        ctx.beginPath();
+        ctx.strokeStyle = color;
+        ctx.lineWidth = 2.0;
+        if (shapeName=="plus") {
+            ctx.moveTo(sourceSize/2., 0);
+            ctx.lineTo(sourceSize/2., sourceSize);
+            ctx.stroke();
+
+            ctx.moveTo(0, sourceSize/2.);
+            ctx.lineTo(sourceSize, sourceSize/2.);
+            ctx.stroke();
+        }
+        else if (shapeName=="cross") {
+            ctx.moveTo(0, 0);
+            ctx.lineTo(sourceSize-1, sourceSize-1);
+            ctx.stroke();
+
+            ctx.moveTo(sourceSize-1, 0);
+            ctx.lineTo(0, sourceSize-1);
+            ctx.stroke();
+        }
+        else if (shapeName=="rhomb") {
+            ctx.moveTo(sourceSize/2, 0);
+            ctx.lineTo(0, sourceSize/2);
+            ctx.lineTo(sourceSize/2, sourceSize);
+            ctx.lineTo(sourceSize, sourceSize/2);
+            ctx.lineTo(sourceSize/2, 0);
+            ctx.stroke();
+        }
+        else if (shapeName=="triangle") {
+            ctx.moveTo(sourceSize/2, 0);
+            ctx.lineTo(0, sourceSize-1);
+            ctx.lineTo(sourceSize-1, sourceSize-1);
+            ctx.lineTo(sourceSize/2, 0);
+            ctx.stroke();
+        }
+        else if (shapeName=="circle") {
+            ctx.arc(sourceSize/2, sourceSize/2, sourceSize/2 - 1, 0, 2*Math.PI, true);
+            ctx.stroke();
+        }
+        else { // default shape: square
+            ctx.moveTo(1, 0);
+            ctx.lineTo(1,  sourceSize-1);
+            ctx.lineTo( sourceSize-1,  sourceSize-1);
+            ctx.lineTo( sourceSize-1, 1);
+            ctx.lineTo(1, 1);
+            ctx.stroke();
+        }
+
+        return c;
+
+    };
+
+
+        // find RA, Dec fields among the given fields
+        //
+        // @param fields: list of objects with ucd, unit, ID, name attributes
+        // @param raField:  index or name of right ascension column (might be undefined)
+        // @param decField: index or name of declination column (might be undefined)
+        //
+        function findRADecFields(fields, raField, decField) {
+            var raFieldIdx,  decFieldIdx;
+            raFieldIdx = decFieldIdx = null;
+
+            // first, look if RA/DEC fields have been already given
+            if (raField) { // ID or name of RA field given at catalogue creation
+                for (var l=0, len=fields.length; l<len; l++) {
+                    var field = fields[l];
+                    if (Utils.isInt(raField) && raField<fields.length) { // raField can be given as an index
+                        raFieldIdx = raField;
+                        break;
+                    }
+                    if ( (field.ID && field.ID===raField) || (field.name && field.name===raField)) {
+                        raFieldIdx = l;
+                        break;
+                    }
+                }
+            }
+            if (decField) { // ID or name of dec field given at catalogue creation
+                for (var l=0, len=fields.length; l<len; l++) {
+                    var field = fields[l];
+                    if (Utils.isInt(decField) && decField<fields.length) { // decField can be given as an index
+                        decFieldIdx = decField;
+                        break;
+                    }
+                    if ( (field.ID && field.ID===decField) || (field.name && field.name===decField)) {
+                        decFieldIdx = l;
+                        break;
+                    }
+                }
+            }
+            // if not already given, let's guess position columns on the basis of UCDs
+            for (var l=0, len=fields.length; l<len; l++) {
+                if (raFieldIdx!=null && decFieldIdx!=null) {
+                    break;
+                }
+
+                var field = fields[l];
+                if ( ! raFieldIdx) {
+                    if (field.ucd) {
+                        var ucd = $.trim(field.ucd.toLowerCase());
+                        if (ucd.indexOf('pos.eq.ra')==0 || ucd.indexOf('pos_eq_ra')==0) {
+                            raFieldIdx = l;
+                            continue;
+                        }
+                    }
+                }
+
+                if ( ! decFieldIdx) {
+                    if (field.ucd) {
+                        var ucd = $.trim(field.ucd.toLowerCase());
+                        if (ucd.indexOf('pos.eq.dec')==0 || ucd.indexOf('pos_eq_dec')==0) {
+                            decFieldIdx = l;
+                            continue;
+                        }
+                    }
+                }
+            }
+
+            // still not found ? try some common names for RA and Dec columns
+            if (raFieldIdx==null && decFieldIdx==null) {
+                for (var l=0, len=fields.length; l<len; l++) {
+                    var field = fields[l];
+                    var name = field.name || field.ID || '';
+                    name = name.toLowerCase();
+
+                    if ( ! raFieldIdx) {
+                        if (name.indexOf('ra')==0 || name.indexOf('_ra')==0 || name.indexOf('ra(icrs)')==0 || name.indexOf('_ra')==0 || name.indexOf('alpha')==0) {
+                            raFieldIdx = l;
+                            continue;
+                        }
+                    }
+
+                    if ( ! decFieldIdx) {
+                        if (name.indexOf('dej2000')==0 || name.indexOf('_dej2000')==0 || name.indexOf('de')==0 || name.indexOf('de(icrs)')==0 || name.indexOf('_de')==0 || name.indexOf('delta')==0) {
+                            decFieldIdx = l;
+                            continue;
+                        }
+                    }
+
+                }
+            }
+
+            // last resort: take two first fieds
+            if (raFieldIdx==null || decFieldIdx==null) {
+                raFieldIdx  = 0;
+                decFieldIdx = 1
+            }
+
+            return [raFieldIdx, decFieldIdx];
+        };
+
+
+
+    // return an array of Source(s) from a VOTable url
+    // callback function is called each time a TABLE element has been parsed
+    cds.Catalog.parseVOTable = function(url, callback, maxNbSources, useProxy, raField, decField) {
+
+        // adapted from votable.js
+        function getPrefix($xml) {
+            var prefix;
+            // If Webkit chrome/safari/... (no need prefix)
+            if($xml.find('RESOURCE').length>0) {
+                prefix = '';
+            }
+            else {
+                // Select all data in the document
+                prefix = $xml.find("*").first();
+
+                if (prefix.length==0) {
+                    return '';
+                }
+
+                // get name of the first tag
+                prefix = prefix.prop("tagName");
+
+                var idx = prefix.indexOf(':');
+
+                prefix = prefix.substring(0, idx) + "\\:";
+
+
+            }
+
+            return prefix;
+        }
+
+        function doParseVOTable(xml, callback) {
+            xml = xml.replace(/^\s+/g, ''); // we need to trim whitespaces at start of document
+            var attributes = ["name", "ID", "ucd", "utype", "unit", "datatype", "arraysize", "width", "precision"];
+
+            var fields = [];
+            var k = 0;
+            var $xml = $($.parseXML(xml));
+            var prefix = getPrefix($xml);
+            $xml.find(prefix + "FIELD").each(function() {
+                var f = {};
+                for (var i=0; i<attributes.length; i++) {
+                    var attribute = attributes[i];
+                    if ($(this).attr(attribute)) {
+                        f[attribute] = $(this).attr(attribute);
+                    }
+                }
+                if ( ! f.ID) {
+                    f.ID = "col_" + k;
+                }
+                fields.push(f);
+                k++;
+            });
+
+            var raDecFieldIdxes = findRADecFields(fields, raField, decField);
+            var raFieldIdx,  decFieldIdx;
+            raFieldIdx = raDecFieldIdxes[0];
+            decFieldIdx = raDecFieldIdxes[1];
+
+            var sources = [];
+
+            var coo = new Coo();
+            var ra, dec;
+            $xml.find(prefix + "TR").each(function() {
+               var mesures = {};
+               var k = 0;
+               $(this).find(prefix + "TD").each(function() {
+                   var key = fields[k].name ? fields[k].name : fields[k].id;
+                   mesures[key] = $(this).text();
+                   k++;
+               });
+               var keyRa = fields[raFieldIdx].name ? fields[raFieldIdx].name : fields[raFieldIdx].id;
+               var keyDec = fields[decFieldIdx].name ? fields[decFieldIdx].name : fields[decFieldIdx].id;
+
+               if (Utils.isNumber(mesures[keyRa]) && Utils.isNumber(mesures[keyDec])) {
+                   ra = parseFloat(mesures[keyRa]);
+                   dec = parseFloat(mesures[keyDec]);
+               }
+               else {
+                   coo.parse(mesures[keyRa] + " " + mesures[keyDec]);
+                   ra = coo.lon;
+                   dec = coo.lat;
+               }
+               sources.push(new cds.Source(ra, dec, mesures));
+               if (maxNbSources && sources.length==maxNbSources) {
+                   return false; // break the .each loop
+               }
+
+            });
+            if (callback) {
+                callback(sources);
+            }
+        }
+
+        var ajax = Utils.getAjaxObject(url, 'GET', 'text', useProxy);
+        ajax.done(function(xml) {
+            doParseVOTable(xml, callback);
+        });
+    };
+
+    // API
+    cds.Catalog.prototype.updateShape = function(options) {
+        options = options || {};
+    	this.color = options.color || this.color || Color.getNextColor();
+    	this.sourceSize = options.sourceSize || this.sourceSize || 6;
+    	this.shape = options.shape || this.shape || "square";
+
+        this.selectSize = this.sourceSize + 2;
+
+        this.cacheCanvas = cds.Catalog.createShape(this.shape, this.color, this.sourceSize);
+        this.cacheSelectCanvas = cds.Catalog.createShape('square', this.selectionColor, this.selectSize);
+
+        this.reportChange();
+    };
+
+    // API
+    cds.Catalog.prototype.addSources = function(sourcesToAdd) {
+        sourcesToAdd = [].concat(sourcesToAdd); // make sure we have an array and not an individual source
+    	this.sources = this.sources.concat(sourcesToAdd);
+    	for (var k=0, len=sourcesToAdd.length; k<len; k++) {
+    	    sourcesToAdd[k].setCatalog(this);
+    	}
+        this.reportChange();
+    };
+
+    // API
+    //
+    // create sources from a 2d array and add them to the catalog
+    //
+    // @param columnNames: array with names of the columns
+    // @array: 2D-array, each item being a 1d-array with the same number of items as columnNames
+    cds.Catalog.prototype.addSourcesAsArray = function(columnNames, array) {
+        var fields = [];
+        for (var colIdx=0 ; colIdx<columnNames.length; colIdx++) {
+            fields.push({name: columnNames[colIdx]});
+        }
+        var raDecFieldIdxes = findRADecFields(fields, this.raField, this.decField);
+        var raFieldIdx,  decFieldIdx;
+        raFieldIdx = raDecFieldIdxes[0];
+        decFieldIdx = raDecFieldIdxes[1];
+
+
+        var newSources = [];
+        var coo = new Coo();
+        var ra, dec, row, dataDict;
+        for (var rowIdx=0 ; rowIdx<array.length ; rowIdx++) {
+            row = array[rowIdx];
+            if (Utils.isNumber(row[raFieldIdx]) && Utils.isNumber(row[decFieldIdx])) {
+                   ra = parseFloat(row[raFieldIdx]);
+                   dec = parseFloat(row[decFieldIdx]);
+            }
+               else {
+                   coo.parse(row[raFieldIdx] + " " + row[decFieldIdx]);
+                   ra = coo.lon;
+                   dec = coo.lat;
+               }
+
+            dataDict = {};
+            for (var colIdx=0 ; colIdx<columnNames.length; colIdx++) {
+                dataDict[columnNames[colIdx]] = row[colIdx];
+            }
+
+            newSources.push(A.source(ra, dec, dataDict));
+        }
+
+        this.addSources(newSources);
+    };
+
+    // return the current list of Source objects
+    cds.Catalog.prototype.getSources = function() {
+        return this.sources;
+    };
+
+    // TODO : fonction générique traversant la liste des sources
+    cds.Catalog.prototype.selectAll = function() {
+        if (! this.sources) {
+            return;
+        }
+
+        for (var k=0; k<this.sources.length; k++) {
+            this.sources[k].select();
+        }
+    };
+
+    cds.Catalog.prototype.deselectAll = function() {
+        if (! this.sources) {
+            return;
+        }
+
+        for (var k=0; k<this.sources.length; k++) {
+            this.sources[k].deselect();
+        }
+    };
+
+    // return a source by index
+    cds.Catalog.prototype.getSource = function(idx) {
+        if (idx<this.sources.length) {
+            return this.sources[idx];
+        }
+        else {
+            return null;
+        }
+    };
+
+    cds.Catalog.prototype.setView = function(view) {
+        this.view = view;
+        this.reportChange();
+    };
+
+    // remove a source
+    cds.Catalog.prototype.remove = function(source) {
+        var idx = this.sources.indexOf(source);
+        if (idx<0) {
+            return;
+        }
+
+        this.sources[idx].deselect();
+        this.sources.splice(idx, 1);
+
+        this.reportChange();
+    };
+
+    cds.Catalog.prototype.removeAll = cds.Catalog.prototype.clear = function() {
+        // TODO : RAZ de l'index
+        this.sources = [];
+    };
+
+    cds.Catalog.prototype.draw = function(ctx, projection, frame, width, height, largestDim, zoomFactor) {
+        if (! this.isShowing) {
+            return;
+        }
+        // tracé simple
+        //ctx.strokeStyle= this.color;
+
+        //ctx.lineWidth = 1;
+    	//ctx.beginPath();
+        if (this._shapeIsFunction) {
+            ctx.save();
+        }
+        var sourcesInView = [];
+ 	    for (var k=0, len = this.sources.length; k<len; k++) {
+		    var inView = cds.Catalog.drawSource(this, this.sources[k], ctx, projection, frame, width, height, largestDim, zoomFactor);
+            if (inView) {
+                sourcesInView.push(this.sources[k]);
+            }
+        }
+        if (this._shapeIsFunction) {
+            ctx.restore();
+        }
+        //ctx.stroke();
+
+    	// tracé sélection
+        ctx.strokeStyle= this.selectionColor;
+        //ctx.beginPath();
+        var source;
+        for (var k=0, len = sourcesInView.length; k<len; k++) {
+            source = sourcesInView[k];
+            if (! source.isSelected) {
+                continue;
+            }
+            cds.Catalog.drawSourceSelection(this, source, ctx);
+
+        }
+        // NEEDED ?
+    	//ctx.stroke();
+
+        // tracé label
+        if (this.displayLabel) {
+            ctx.fillStyle = this.labelColor;
+            ctx.font = this.labelFont;
+            for (var k=0, len = sourcesInView.length; k<len; k++) {
+                cds.Catalog.drawSourceLabel(this, sourcesInView[k], ctx);
+            }
+        }
+    };
+
+
+
+    cds.Catalog.drawSource = function(catalogInstance, s, ctx, projection, frame, width, height, largestDim, zoomFactor) {
+        if (! s.isShowing) {
+            return false;
+        }
+        var sourceSize = catalogInstance.sourceSize;
+        // TODO : we could factorize this code with Aladin.world2pix
+        var xy;
+        if (frame.system != CooFrameEnum.SYSTEMS.J2000) {
+            var lonlat = CooConversion.J2000ToGalactic([s.ra, s.dec]);
+            xy = projection.project(lonlat[0], lonlat[1]);
+        }
+        else {
+            xy = projection.project(s.ra, s.dec);
+        }
+
+        if (xy) {
+            var xyview = AladinUtils.xyToView(xy.X, xy.Y, width, height, largestDim, zoomFactor, true);
+            var max = s.popup ? 100 : s.sourceSize;
+            if (xyview) {
+                // TODO : index sources by HEALPix cells at level 3, 4 ?
+
+                // check if source is visible in view
+                if (xyview.vx>(width+max)  || xyview.vx<(0-max) ||
+                    xyview.vy>(height+max) || xyview.vy<(0-max)) {
+                    s.x = s.y = undefined;
+                    return false;
+                }
+
+                s.x = xyview.vx;
+                s.y = xyview.vy;
+                if (catalogInstance._shapeIsFunction) {
+                    catalogInstance.shape(s, ctx, catalogInstance.view.getViewParams());
+                }
+                else if (s.marker && s.useMarkerDefaultIcon) {
+                    ctx.drawImage(catalogInstance.cacheMarkerCanvas, s.x-sourceSize/2, s.y-sourceSize/2);
+                }
+                else {
+                    ctx.drawImage(catalogInstance.cacheCanvas, s.x-catalogInstance.cacheCanvas.width/2, s.y-catalogInstance.cacheCanvas.height/2);
+                }
+
+
+                // has associated popup ?
+                if (s.popup) {
+                    s.popup.setPosition(s.x, s.y);
+                }
+
+
+            }
+            return true;
+        }
+        else {
+            return false;
+        }
+
+
+    };
+
+    cds.Catalog.drawSourceSelection = function(catalogInstance, s, ctx) {
+        if (!s || !s.isShowing || !s.x || !s.y) {
+            return;
+        }
+        var sourceSize = catalogInstance.selectSize;
+
+        ctx.drawImage(catalogInstance.cacheSelectCanvas, s.x-sourceSize/2, s.y-sourceSize/2);
+    };
+
+    cds.Catalog.drawSourceLabel = function(catalogInstance, s, ctx) {
+        if (!s || !s.isShowing || !s.x || !s.y) {
+            return;
+        }
+
+        var label = s.data[catalogInstance.labelColumn];
+        if (!label) {
+            return;
+        }
+
+        ctx.fillText(label, s.x, s.y);
+    };
+
+
+    // callback function to be called when the status of one of the sources has changed
+    cds.Catalog.prototype.reportChange = function() {
+        this.view && this.view.requestRedraw();
+    };
+
+    cds.Catalog.prototype.show = function() {
+        if (this.isShowing) {
+            return;
+        }
+        this.isShowing = true;
+        this.reportChange();
+    };
+
+    cds.Catalog.prototype.hide = function() {
+        if (! this.isShowing) {
+            return;
+        }
+        this.isShowing = false;
+        if (this.view && this.view.popup && this.view.popup.source && this.view.popup.source.catalog==this) {
+            this.view.popup.hide();
+        }
+
+        this.reportChange();
+    };
+
+    return cds.Catalog;
+})();
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File ProgressiveCat.js
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+// TODO: index sources according to their HEALPix ipix
+// TODO : merge parsing with class Catalog
+ProgressiveCat = (function() {
+
+    // TODO : test if CORS support. If no, need to pass through a proxy
+    // currently, we suppose CORS is supported
+
+    // constructor
+    ProgressiveCat = function(rootUrl, frameStr, maxOrder, options) {
+        options = options || {};
+
+        this.type = 'progressivecat';
+
+        this.rootUrl = rootUrl; // TODO: method to sanitize rootURL (absolute, no duplicate slashes, remove end slash if existing)
+        // fast fix for HTTPS support --> will work for all HiPS served by CDS
+        if (Utils.isHttpsContext() && ( /u-strasbg.fr/i.test(this.rootUrl) || /unistra.fr/i.test(this.rootUrl)  ) ) {
+            this.rootUrl = this.rootUrl.replace('http://', 'https://');
+        }
+
+        this.frameStr = frameStr;
+        this.frame = CooFrameEnum.fromString(frameStr) || CooFrameEnum.J2000;
+        this.maxOrder = maxOrder;
+        this.isShowing = true; // TODO : inherit from catalogue
+
+        this.name = options.name || "progressive-cat";
+        this.color = options.color || Color.getNextColor();
+        this.shape = options.shape || "square";
+        this.sourceSize = options.sourceSize || 6;
+        this.selectSize = this.sourceSize + 2;
+        this.selectionColor = '#00ff00'; // TODO: to be merged with Catalog
+
+        // allows for filtering of sources
+        this.filterFn = options.filter || undefined; // TODO: do the same for catalog
+
+
+        this.onClick = options.onClick || undefined; // TODO: inherit from catalog
+
+
+
+        // we cache the list of sources in each healpix tile. Key of the cache is norder+'-'+npix
+        this.sourcesCache = new Utils.LRUCache(100);
+
+        this.updateShape(options);
+
+
+
+
+        this.maxOrderAllsky = 2;
+        this.isReady = false;
+    };
+
+    // TODO: to be put higher in the class diagram, in a HiPS generic class
+    ProgressiveCat.readProperties = function(rootUrl, successCallback, errorCallback) {
+        if (! successCallback) {
+            return;
+        }
+
+        var propertiesURL = rootUrl + '/properties';
+        $.ajax({
+            url: propertiesURL,
+            method: 'GET',
+            dataType: 'text',
+            success: function(propertiesTxt) {
+                var props = {};
+                var lines = propertiesTxt.split('\n');
+                for (var k=0; k<lines.length; k++) {
+                    var line = lines[k];
+                    var idx = line.indexOf('=');
+                    var propName  = $.trim(line.substring(0, idx));
+                    var propValue = $.trim(line.substring(idx + 1));
+
+                    props[propName] = propValue;
+                }
+
+                successCallback(props);
+
+            },
+            error: function(err) { // TODO : which parameters should we put in the error callback
+                errorCallback && errorCallback(err);
+            }
+        });
+
+
+
+
+
+    };
+
+    function getFields(instance, xml) {
+        var attributes = ["name", "ID", "ucd", "utype", "unit", "datatype", "arraysize", "width", "precision"];
+
+        var fields = [];
+        var k = 0;
+        instance.keyRa = instance.keyDec = null;
+        $(xml).find("FIELD").each(function() {
+            var f = {};
+            for (var i=0; i<attributes.length; i++) {
+                var attribute = attributes[i];
+                if ($(this).attr(attribute)) {
+                    f[attribute] = $(this).attr(attribute);
+                }
+
+            }
+            if ( ! f.ID) {
+                f.ID = "col_" + k;
+            }
+
+            if (!instance.keyRa && f.ucd && (f.ucd.indexOf('pos.eq.ra')==0 || f.ucd.indexOf('POS_EQ_RA')==0)) {
+                if (f.name) {
+                    instance.keyRa = f.name;
+                }
+                else {
+                    instance.keyRa = f.ID;
+                }
+            }
+            if (!instance.keyDec && f.ucd && (f.ucd.indexOf('pos.eq.dec')==0 || f.ucd.indexOf('POS_EQ_DEC')==0)) {
+                if (f.name) {
+                    instance.keyDec = f.name;
+                }
+                else {
+                    instance.keyDec = f.ID;
+                }
+            }
+
+            fields.push(f);
+            k++;
+        });
+
+        return fields;
+    }
+
+    function getSources(instance, csv, fields) {
+        // TODO : find ra and dec key names (see in Catalog)
+        if (!instance.keyRa || ! instance.keyDec) {
+            return [];
+        }
+        lines = csv.split('\n');
+        var mesureKeys = [];
+        for (var k=0; k<fields.length; k++) {
+            if (fields[k].name) {
+                mesureKeys.push(fields[k].name);
+            }
+            else {
+                mesureKeys.push(fields[k].ID);
+            }
+        }
+
+
+        var sources = [];
+        var coo = new Coo();
+        var newSource;
+        // start at i=1, as first line repeat the fields names
+        for (var i=2; i<lines.length; i++) {
+            var mesures = {};
+            var data = lines[i].split('\t');
+            if (data.length<mesureKeys.length) {
+                continue;
+            }
+            for (var j=0; j<mesureKeys.length; j++) {
+                mesures[mesureKeys[j]] = data[j];
+            }
+            var ra, dec;
+            if (Utils.isNumber(mesures[instance.keyRa]) && Utils.isNumber(mesures[instance.keyDec])) {
+                ra = parseFloat(mesures[instance.keyRa]);
+                dec = parseFloat(mesures[instance.keyDec]);
+            }
+            else {
+                coo.parse(mesures[instance.keyRa] + " " + mesures[instance.keyDec]);
+                ra = coo.lon;
+                dec = coo.lat;
+            }
+            newSource = new cds.Source(ra, dec, mesures);
+            sources.push(newSource);
+            newSource.setCatalog(instance);
+        }
+        return sources;
+    };
+
+    //ProgressiveCat.prototype.updateShape = cds.Catalog.prototype.updateShape;
+
+    ProgressiveCat.prototype = {
+
+        init: function(view) {
+            var self = this;
+            this.view = view;
+
+            if (this.maxOrder && this.frameStr) {
+                this._loadMetadata();
+            }
+
+            else {
+                ProgressiveCat.readProperties(self.rootUrl,
+                    function (properties) {
+                        self.properties = properties;
+                        self.maxOrder = self.properties['hips_order'];
+                        self.frame = CooFrameEnum.fromString(self.properties['hips_frame']);
+
+                        self._loadMetadata();
+                    }, function(err) {
+                        console.log('Could not find properties for HiPS ' + self.rootUrl);
+                    }
+                );
+            }
+        },
+
+        updateShape: cds.Catalog.prototype.updateShape,
+
+        _loadMetadata: function() {
+            var self = this;
+            $.ajax({
+                url: self.rootUrl + '/' + 'Metadata.xml',
+                method: 'GET',
+                success: function(xml) {
+                    self.fields = getFields(self, xml);
+                    self._loadAllskyNewMethod();
+                },
+                error: function(err) {
+                    self._loadAllskyOldMethod();
+                }
+            });
+        },
+
+        _loadAllskyNewMethod: function() {
+            var self = this;
+            $.ajax({
+                url: self.rootUrl + '/' + 'Norder1/Allsky.tsv',
+                method: 'GET',
+                success: function(tsv) {
+                    self.order1Sources = getSources(self, tsv, self.fields);
+
+                    if (self.order2Sources) {
+                        self.isReady = true;
+                        self._finishInitWhenReady();
+                    }
+                },
+                error: function(err) {
+                    console.log('Something went wrong: ' + err);
+                }
+            });
+
+            $.ajax({
+                url: self.rootUrl + '/' + 'Norder2/Allsky.tsv',
+                method: 'GET',
+                success: function(tsv) {
+                    self.order2Sources = getSources(self, tsv, self.fields);
+
+                    if (self.order1Sources) {
+                        self.isReady = true;
+                        self._finishInitWhenReady();
+                    }
+                },
+                error: function(err) {
+                    console.log('Something went wrong: ' + err);
+                }
+            });
+
+        },
+
+        _loadAllskyOldMethod: function() {
+            this.maxOrderAllsky = 3;
+            this._loadLevel2Sources();
+            this._loadLevel3Sources();
+        },
+
+        _loadLevel2Sources: function() {
+            var self = this;
+            $.ajax({
+                url: self.rootUrl + '/' + 'Norder2/Allsky.xml',
+                method: 'GET',
+                success: function(xml) {
+                    self.fields = getFields(self, xml);
+                    self.order2Sources = getSources(self, $(xml).find('CSV').text(), self.fields);
+                    if (self.order3Sources) {
+                        self.isReady = true;
+                        self._finishInitWhenReady();
+                    }
+                },
+                error: function(err) {
+                    console.log('Something went wrong: ' + err);
+                }
+            });
+        },
+
+        _loadLevel3Sources: function() {
+            var self = this;
+            $.ajax({
+                url: self.rootUrl + '/' + 'Norder3/Allsky.xml',
+                method: 'GET',
+                success: function(xml) {
+                    self.order3Sources = getSources(self, $(xml).find('CSV').text(), self.fields);
+                    if (self.order2Sources) {
+                        self.isReady = true;
+                        self._finishInitWhenReady();
+                    }
+                },
+                error: function(err) {
+                    console.log('Something went wrong: ' + err);
+                }
+            });
+        },
+
+        _finishInitWhenReady: function() {
+            this.view.requestRedraw();
+            this.loadNeededTiles();
+        },
+
+        draw: function(ctx, projection, frame, width, height, largestDim, zoomFactor) {
+            if (! this.isShowing || ! this.isReady) {
+                return;
+            }
+            this.drawSources(this.order1Sources, ctx, projection, frame, width, height, largestDim, zoomFactor);
+            this.drawSources(this.order2Sources, ctx, projection, frame, width, height, largestDim, zoomFactor);
+            this.drawSources(this.order3Sources, ctx, projection, frame, width, height, largestDim, zoomFactor);
+
+            if (!this.tilesInView) {
+                return;
+            }
+            var sources, key, t;
+            for (var k=0; k<this.tilesInView.length; k++) {
+                t = this.tilesInView[k];
+                key = t[0] + '-' + t[1];
+                sources = this.sourcesCache.get(key);
+                if (sources) {
+                    this.drawSources(sources, ctx, projection, frame, width, height, largestDim, zoomFactor);
+                }
+            }
+
+
+
+        },
+        drawSources: function(sources, ctx, projection, frame, width, height, largestDim, zoomFactor) {
+            if (! sources) {
+                return;
+            }
+            var s;
+            for (var k=0, len = sources.length; k<len; k++) {
+                s = sources[k];
+                if (!this.filterFn || this.filterFn(s)) {
+                    cds.Catalog.drawSource(this, s, ctx, projection, frame, width, height, largestDim, zoomFactor);
+                }
+            }
+            for (var k=0, len = sources.length; k<len; k++) {
+                s = sources[k];
+                if (! s.isSelected) {
+                    continue;
+                }
+                if (!this.filterFn || this.filterFn(s)) {
+                    cds.Catalog.drawSourceSelection(this, s, ctx);
+                }
+            }
+        },
+
+        getSources: function() {
+            var ret = [];
+            if (this.order1Sources) {
+                ret = ret.concat(this.order1Sources);
+            }
+            if (this.order2Sources) {
+                ret = ret.concat(this.order2Sources);
+            }
+            if (this.order3Sources) {
+                ret = ret.concat(this.order3Sources);
+            }
+            if (this.tilesInView) {
+                var sources, key, t;
+                for (var k=0; k<this.tilesInView.length; k++) {
+                    t = this.tilesInView[k];
+                    key = t[0] + '-' + t[1];
+                    sources = this.sourcesCache.get(key);
+                    if (sources) {
+                        ret = ret.concat(sources);
+                    }
+                }
+            }
+
+            return ret;
+        },
+
+
+
+        deselectAll: function() {
+            if (this.order1Sources) {
+                for (var k=0; k<this.order1Sources.length; k++) {
+                    this.order1Sources[k].deselect();
+                }
+            }
+
+            if (this.order2Sources) {
+                for (var k=0; k<this.order2Sources.length; k++) {
+                    this.order2Sources[k].deselect();
+                }
+            }
+
+            if (this.order3Sources) {
+                for (var k=0; k<this.order3Sources.length; k++) {
+                    this.order3Sources[k].deselect();
+                }
+            }
+            var keys = this.sourcesCache.keys();
+            for (key in keys) {
+                if ( ! this.sourcesCache[key]) {
+                    continue;
+                }
+                var sources = this.sourcesCache[key];
+                for (var k=0; k<sources.length; k++) {
+                    sources[k].deselect();
+                }
+            }
+        },
+
+        show: function() {
+            if (this.isShowing) {
+                return;
+            }
+            this.isShowing = true;
+            this.loadNeededTiles();
+            this.reportChange();
+        },
+        hide: function() {
+            if (! this.isShowing) {
+                return;
+            }
+            this.isShowing = false;
+            this.reportChange();
+        },
+        reportChange: function() {
+            this.view.requestRedraw();
+        },
+
+        getTileURL: function(norder, npix) {
+            var dirIdx = Math.floor(npix/10000)*10000;
+            return this.rootUrl + "/" + "Norder" + norder + "/Dir" + dirIdx + "/Npix" + npix + ".tsv";
+        },
+
+        loadNeededTiles: function() {
+            if ( ! this.isShowing) {
+                return;
+            }
+            this.tilesInView = [];
+
+            var norder = this.view.realNorder;
+            if (norder>this.maxOrder) {
+                norder = this.maxOrder;
+            }
+            if (norder<=this.maxOrderAllsky) {
+                return; // nothing to do, hurrayh !
+            }
+            var cells = this.view.getVisibleCells(norder, this.frame);
+            var ipixList, ipix;
+            for (var curOrder=3; curOrder<=norder; curOrder++) {
+                ipixList = [];
+                for (var k=0; k<cells.length; k++) {
+                    ipix = Math.floor(cells[k].ipix / Math.pow(4, norder - curOrder));
+                    if (ipixList.indexOf(ipix)<0) {
+                        ipixList.push(ipix);
+                    }
+                }
+
+                // load needed tiles
+                for (var i=0; i<ipixList.length; i++) {
+                    this.tilesInView.push([curOrder, ipixList[i]]);
+                }
+            }
+
+            var t, key;
+            var self = this;
+            for (var k=0; k<this.tilesInView.length; k++) {
+                t = this.tilesInView[k];
+                key = t[0] + '-' + t[1]; // t[0] is norder, t[1] is ipix
+                if (!this.sourcesCache.get(key)) {
+                    (function(self, norder, ipix) { // wrapping function is needed to be able to retrieve norder and ipix in ajax success function
+                        var key = norder + '-' + ipix;
+                        $.ajax({
+                            /*
+                            url: Aladin.JSONP_PROXY,
+                            data: {"url": self.getTileURL(norder, ipix)},
+                            */
+                            // ATTENTIOn : je passe en JSON direct, car je n'arrive pas a choper les 404 en JSONP
+                            url: self.getTileURL(norder, ipix),
+                            method: 'GET',
+                            //dataType: 'jsonp',
+                            success: function(tsv) {
+                                self.sourcesCache.set(key, getSources(self, tsv, self.fields));
+                                self.view.requestRedraw();
+                            },
+                            error: function() {
+                                // on suppose qu'il s'agit d'une erreur 404
+                                self.sourcesCache.set(key, []);
+                            }
+                        });
+                    })(this, t[0], t[1]);
+                }
+            }
+        },
+
+        reportChange: function() { // TODO: to be shared with Catalog
+            this.view && this.view.requestRedraw();
+        }
+
+
+    }; // END OF .prototype functions
+
+
+    return ProgressiveCat;
+})();
+
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File Tile
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+Tile = (function() {
+    // constructor
+	function Tile(img, url) {
+		this.img = img;
+		this.url = url;
+	};
+
+	// check whether the image corresponding to the tile is loaded and ready to be displayed
+	//
+	// source : http://www.sajithmr.me/javascript-check-an-image-is-loaded-or-not
+	Tile.isImageOk = function(img) {
+		if (img.allSkyTexture) {
+			return true;
+		}
+
+        if (!img.src) {
+            return false;
+        }
+
+	    // During the onload event, IE correctly identifies any images that
+	    // weren’t downloaded as not complete. Others should too. Gecko-based
+	    // browsers act like NS4 in that they report this incorrectly.
+	    if (!img.complete) {
+	        return false;
+	    }
+
+	    // However, they do have two very useful properties: naturalWidth and
+	    // naturalHeight. These give the true size of the image. If it failed
+	    // to load, either of these should be zero.
+
+	    if (typeof img.naturalWidth != "undefined" && img.naturalWidth == 0) {
+	        return false;
+	    }
+
+	    // No other way of checking: assume it’s ok.
+	    return true;
+	};
+
+
+	return Tile;
+})();
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File TileBuffer
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+TileBuffer = (function() {
+	var NB_MAX_TILES = 800; // buffer size
+
+	// constructor
+	function TileBuffer() {
+		this.pointer = 0;
+		this.tilesMap = {};
+		this.tilesArray = new Array(NB_MAX_TILES);
+
+		for (var i=0; i<NB_MAX_TILES; i++) {
+			this.tilesArray[i] = new Tile(new Image(), null);
+		}
+	};
+
+	TileBuffer.prototype.addTile = function(url) {
+	    // return null if already in buffer
+        if (this.getTile(url)) {
+            return null;
+        }
+
+        // delete existing tile
+        var curTile = this.tilesArray[this.pointer];
+        if (curTile.url != null) {
+            curTile.img.src = null;
+            delete this.tilesMap[curTile.url];
+        }
+
+        this.tilesArray[this.pointer].url = url;
+        this.tilesMap[url] = this.tilesArray[this.pointer];
+
+        this.pointer++;
+        if (this.pointer>=NB_MAX_TILES) {
+            this.pointer = 0;
+        }
+
+        return this.tilesMap[url];
+	};
+
+	TileBuffer.prototype.getTile = function(url) {
+        return this.tilesMap[url];
+	};
+
+	return TileBuffer;
+})();
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File ColorMap.js
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+ColorMap = (function() {
+
+
+    // constructor
+    ColorMap = function(view) {
+        this.view = view;
+        this.reversed = false;
+        this.mapName = 'native';
+        this.sig = this.signature();
+    };
+
+ColorMap.MAPS = {};
+
+    ColorMap.MAPS['eosb'] = {
+            name: 'Eos B',
+            r: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,9,18,27,36,45,49,57,72,81,91,100,109,118,127,
+                136,131,139,163,173,182,191,200,209,218,227,213,221,255,255,255,255,255,
+                255,255,255,229,229,255,255,255,255,255,255,255,255,229,229,255,255,255,
+                255,255,255,255,255,229,229,255,255,255,255,255,255,255,255,229,229,255,
+                255,255,255,255,255,255,255,229,229,255,255,255,255,255,255,255,255,229,
+                229,255,255,255,255,255,255,255,255,229,229,255,255,255,255,255,255,255,
+                255,229,229,255,255,255,255,255,255,255,255,229,229,255,253,251,249,247,
+                245,243,241,215,214,235,234,232,230,228,226,224,222,198,196,216,215,213,
+                211,209,207,205,203,181,179,197,196,194,192,190,188,186,184,164,162,178,
+                176,175,173,171,169,167,165,147,145,159,157,156,154,152,150,148,146,130,
+                128,140,138,137,135,133,131,129,127,113,111,121,119,117,117],
+            g: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,15,23,31,39,47,55,57,64,79,87,95,
+                103,111,119,127,135,129,136,159,167,175,183,191,199,207,215,200,207,239,
+                247,255,255,255,255,255,255,229,229,255,255,255,255,255,255,255,255,229,
+                229,255,255,255,255,255,255,255,255,229,229,255,250,246,242,238,233,229,
+                225,198,195,212,208,204,199,195,191,187,182,160,156,169,165,161,157,153,
+                148,144,140,122,118,127,125,123,121,119,116,114,112,99,97,106,104,102,
+                99,97,95,93,91,80,78,84,82,80,78,76,74,72,70,61,59,63,61,59,57,55,53,50,
+                48,42,40,42,40,38,36,33,31,29,27,22,21,21,19,16,14,12,13,8,6,3,1,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
+            b: [116,121,127,131,136,140,144,148,153,
+                157,145,149,170,174,178,182,187,191,195,199,183,187,212,216,221,225,229,
+                233,238,242,221,225,255,247,239,231,223,215,207,199,172,164,175,167,159,
+                151,143,135,127,119,100,93,95,87,79,71,63,55,47,39,28,21,15,7,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0]
+    };
+    ColorMap.MAPS['rainbow'] = {
+            name: 'Rainbow',
+            r: [0,4,9,13,18,22,27,31,36,40,45,50,54,
+                58,61,64,68,69,72,74,77,79,80,82,83,85,84,86,87,88,86,87,87,87,85,84,84,
+                84,83,79,78,77,76,71,70,68,66,60,58,55,53,46,43,40,36,33,25,21,16,12,4,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,12,21,25,29,33,42,
+                46,51,55,63,67,72,76,80,89,93,97,101,110,114,119,123,131,135,140,144,153,
+                157,161,165,169,178,182,187,191,199,203,208,212,221,225,229,233,242,246,
+                250,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+                255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+                255,255,255,255,255,255,255,255,255,255,255,255,255,255],
+            g: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,4,8,16,21,25,29,38,42,46,51,55,63,67,72,76,84,89,93,97,
+                106,110,114,119,127,131,135,140,144,152,157,161,165,174,178,182,187,195,
+                199,203,208,216,220,225,229,233,242,246,250,255,255,255,255,255,255,255,
+                255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+                255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+                255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+                255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+                255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+                255,250,242,238,233,229,221,216,212,208,199,195,191,187,178,174,170,165,
+                161,153,148,144,140,131,127,123,119,110,106,102,97,89,85,80,76,72,63,59,
+                55,51,42,38,34,29,21,17,12,8,0],
+            b: [0,3,7,10,14,19,23,28,32,38,43,48,53,
+                59,63,68,72,77,81,86,91,95,100,104,109,113,118,122,127,132,136,141,145,
+                150,154,159,163,168,173,177,182,186,191,195,200,204,209,214,218,223,227,
+                232,236,241,245,250,255,255,255,255,255,255,255,255,255,255,255,255,255,
+                255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+                255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+                255,255,255,255,255,255,246,242,238,233,225,220,216,212,203,199,195,191,
+                187,178,174,170,165,157,152,148,144,135,131,127,123,114,110,106,102,97,
+                89,84,80,76,67,63,59,55,46,42,38,34,25,21,16,12,8,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
+    };
+    ColorMap.MAPS['cubehelix'] = {
+            name: 'Cubehelix',
+            r: [0,1,3,4,6,8,9,10,12,13,14,15,17,18,
+                19,20,20,21,22,23,23,24,24,25,25,25,26,26,26,26,26,26,26,26,26,26,26,25,
+                25,25,25,24,24,24,23,23,23,23,22,22,22,21,21,21,21,21,21,20,20,20,21,21,
+                21,21,21,22,22,22,23,23,24,25,26,27,27,28,30,31,32,33,35,36,38,39,41,43,
+                45,47,49,51,53,55,57,60,62,65,67,70,72,75,78,81,83,86,89,92,95,98,101,104,
+                107,110,113,116,120,123,126,129,132,135,138,141,144,147,150,153,155,158,
+                161,164,166,169,171,174,176,178,181,183,185,187,189,191,193,194,196,198,
+                199,201,202,203,204,205,206,207,208,209,209,210,211,211,211,212,212,212,
+                212,212,212,212,212,211,211,211,210,210,210,209,208,208,207,207,206,205,
+                205,204,203,203,202,201,201,200,199,199,198,197,197,196,196,195,195,194,
+                194,194,193,193,193,193,193,193,193,193,193,193,194,194,195,195,196,196,
+                197,198,199,200,200,202,203,204,205,206,208,209,210,212,213,215,217,218,
+                220,222,223,225,227,229,231,232,234,236,238,240,242,244,245,247,249,251,
+                253,255],
+            g: [0,0,1,1,2,2,3,4,4,5,6,6,7,8,9,10,
+                11,11,12,13,14,15,17,18,19,20,21,22,24,25,26,28,29,31,32,34,35,37,38,40,
+                41,43,45,46,48,50,52,53,55,57,58,60,62,64,66,67,69,71,73,74,76,78,79,81,
+                83,84,86,88,89,91,92,94,95,97,98,99,101,102,103,104,106,107,108,109,110,
+                111,112,113,114,114,115,116,116,117,118,118,119,119,120,120,120,121,121,
+                121,121,122,122,122,122,122,122,122,122,122,122,122,122,122,122,122,121,
+                121,121,121,121,121,121,121,121,120,120,120,120,120,120,120,120,120,120,
+                121,121,121,121,121,122,122,122,123,123,124,124,125,125,126,127,127,128,
+                129,130,131,131,132,133,135,136,137,138,139,140,142,143,144,146,147,149,
+                150,152,154,155,157,158,160,162,164,165,167,169,171,172,174,176,178,180,
+                182,183,185,187,189,191,193,194,196,198,200,202,203,205,207,208,210,212,
+                213,215,216,218,219,221,222,224,225,226,228,229,230,231,232,233,235,236,
+                237,238,239,240,240,241,242,243,244,244,245,246,247,247,248,248,249,250,
+                250,251,251,252,252,253,253,254,255],
+            b: [0,1,3,4,6,8,9,11,13,15,17,19,21,23,
+                25,27,29,31,33,35,37,39,41,43,45,47,48,50,52,54,56,57,59,60,62,63,65,66,
+                67,69,70,71,72,73,74,74,75,76,76,77,77,77,78,78,78,78,78,78,78,77,77,77,
+                76,76,75,75,74,73,73,72,71,70,69,68,67,66,66,65,64,63,61,60,59,58,58,57,
+                56,55,54,53,52,51,51,50,49,49,48,48,47,47,47,46,46,46,46,46,47,47,47,48,
+                48,49,50,50,51,52,53,55,56,57,59,60,62,64,65,67,69,71,74,76,78,81,83,86,
+                88,91,94,96,99,102,105,108,111,114,117,120,124,127,130,133,136,140,143,
+                146,149,153,156,159,162,165,169,172,175,178,181,184,186,189,192,195,197,
+                200,203,205,207,210,212,214,216,218,220,222,224,226,227,229,230,231,233,
+                234,235,236,237,238,239,239,240,241,241,242,242,242,243,243,243,243,243,
+                243,243,243,243,243,242,242,242,242,241,241,241,241,240,240,240,239,239,
+                239,239,239,238,238,238,238,238,238,238,238,239,239,239,240,240,240,241,
+                242,242,243,244,245,246,247,248,249,250,252,253,255]
+    };
+
+
+
+    ColorMap.MAPS_CUSTOM = ['cubehelix', 'eosb', 'rainbow'];
+    ColorMap.MAPS_NAMES = ['native', 'grayscale'].concat(ColorMap.MAPS_CUSTOM);
+
+    ColorMap.prototype.reverse = function(val) {
+        if (val) {
+            this.reversed = val;
+        }
+        else {
+            this.reversed = ! this.reversed;
+        }
+        this.sig = this.signature();
+        this.view.requestRedraw();
+    };
+
+
+    ColorMap.prototype.signature = function() {
+        var s = this.mapName;
+
+        if (this.reversed) {
+            s += ' reversed';
+        }
+
+        return s;
+    };
+
+    ColorMap.prototype.update = function(mapName) {
+        this.mapName = mapName;
+        this.sig = this.signature();
+        this.view.requestRedraw();
+    };
+
+    ColorMap.prototype.apply = function(img) {
+        if ( this.sig=='native' ) {
+            return img;
+        }
+
+        if (img.cmSig==this.sig) {
+            return img.cmImg; // return cached pixels
+        }
+
+        var canvas = document.createElement("canvas");
+        canvas.width = img.width;
+        canvas.height = img.height;
+        var ctx = canvas.getContext("2d");
+        ctx.drawImage(img, 0, 0);
+
+        var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+        var pixelData = imageData.data;
+        var length = pixelData.length;
+        var a, b, c;
+        var switchCase = 3;
+        if (this.mapName=='grayscale') {
+            switchCase = 1;
+        }
+        else if (ColorMap.MAPS_CUSTOM.indexOf(this.mapName)>=0) {
+            switchCase = 2;
+        }
+        for (var i = 0; i < length; i+= 4) {
+            switch(switchCase) {
+                case 1:
+                    a = b = c = AladinUtils.myRound((pixelData[i]+pixelData[i+1]+pixelData[i+2])/3);
+                    break;
+                case 2:
+                    if (this.reversed) {
+                        a = ColorMap.MAPS[this.mapName].r[255-pixelData[i]];
+                        b = ColorMap.MAPS[this.mapName].g[255-pixelData[i+1]];
+                        c = ColorMap.MAPS[this.mapName].b[255-pixelData[i+2]];
+                    }
+                    else {
+                        a = ColorMap.MAPS[this.mapName].r[pixelData[i]];
+                        b = ColorMap.MAPS[this.mapName].g[pixelData[i+1]];
+                        c = ColorMap.MAPS[this.mapName].b[pixelData[i+2]];
+                    }
+                    break;
+                default:
+                    a = pixelData[i];
+                    b = pixelData[i + 1];
+                    c = pixelData[i + 2];
+
+            }
+            if (switchCase!=2 && this.reversed) {
+                a = 255-a;
+                b = 255-b;
+                c = 255-c;
+
+            }
+            pixelData[i]     = a;
+            pixelData[i + 1] = b;
+            pixelData[i + 2] = c;
+
+        }
+        imageData.data = pixelData;
+        ctx.putImageData(imageData, 0, 0);
+
+        // cache image with color map applied
+        img.cmSig = this.sig;
+        img.cmImg = canvas;
+
+        return img.cmImg;
+    };
+
+    return ColorMap;
+})();
+
+// Copyright 2016 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File HpxKey
+ * This class represents a HEALPix cell
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+HpxKey = (function() {
+
+    "use strict";
+
+    /** Constructor
+     *
+     */
+    var HpxKey = function(norder, npix, hips, width, height, dx, dy, allskyTexture, allskyTextureSize) {
+        this.norder = norder;
+        this.npix = npix;
+
+        this.nside = Math.pow(2, norder);
+
+        this.hips = hips; // survey to which this HpxKey is attached
+        this.frame = hips.cooFrame; // coordinate frame of the survey to which this HpxKey is attached
+
+        this.width = width; // width of the tile
+        this.height = height; // height of the tile
+
+        this.dx = dx || 0; // shift in x (for all-sky tiles)
+        this.dy = dy || 0; // shift in y (for all-sky tiles)
+
+        this.allskyTexture = allskyTexture || undefined;
+        this.allskyTextureSize = allskyTextureSize;
+
+        this.parente = 0; // if this key comes from an ancestor, length of the filiation
+
+        this.children = null;
+        this.ancestor = null; // ancestor having the pixels
+    }
+
+    // "static" methods
+    HpxKey.createHpxKeyfromAncestor = function(father, childNb) {
+        var hpxKey = new HpxKey(father.norder+1, father.npix*4 + childNb, father.hips, father.width/2, father.height/2,
+                                childNb==2 || childNb==3 ? father.dx+father.width/2 : father.dx, childNb==1 || childNb==3 ? father.dy+father.height/2 : father.dy, father.allskyTexture, father.allskyTextureSize);
+        hpxKey.parente = father.parente + 1;
+        hpxKey.ancestor = father.ancestor || father;
+
+
+        return hpxKey;
+    };
+
+    var MAX_PARENTE = 4;
+
+    HpxKey.prototype = {
+
+        draw: function(ctx, view) {
+//console.log('Drawing ', this.norder, this.npix);
+            var n = 0; // number of traced triangles
+            var corners = this.getProjViewCorners(view);
+
+            if (corners==null) {
+                return 0;
+            }
+
+
+            var now = new Date().getTime();
+            var updateNeededTiles = this.ancestor==null && this.norder>=3 && (now-this.hips.lastUpdateDateNeededTiles) > 0.1;
+
+            try {
+                if (isTooLarge(corners)) {
+//console.log('too large');
+                    var m = this.drawChildren(ctx, view, MAX_PARENTE);
+
+                    // Si aucun sous-losange n'a pu être dessiné, je trace tout de même le père
+                    if( m>0 ) {
+                        return m;
+                    }
+                }
+            }
+            catch(e) {
+                return 0;
+            }
+
+
+            // actual drawing
+            var norder = this.ancestor==null ? this.norder : this.ancestor.norder;
+            var npix = this.ancestor==null ? this.npix : this.ancestor.npix;
+
+            //console.log(corners);
+            //corners = AladinUtils.grow2(corners, 1); // grow by 1 pixel in each direction
+            //console.log(corners);
+            var url = this.hips.getTileURL(norder, npix);
+            var tile = this.hips.tileBuffer.getTile(url);
+            if (tile && Tile.isImageOk(tile.img) || this.allskyTexture) {
+                if (!this.allskyTexture && !this.hips.tileSize) {
+                    this.hips.tileSize = tile.img.width;
+                }
+                var img = this.allskyTexture || tile.img;
+                var w = this.allskyTextureSize || img.width;
+                if (this.parente) {
+                    w = w / Math.pow(2, this.parente);
+                }
+
+                this.hips.drawOneTile2(ctx, img, corners, w, null, this.dx, this.dy, true, norder);
+                n += 2;
+            }
+            else if (updateNeededTiles && ! tile) {
+                tile = this.hips.tileBuffer.addTile(url);
+                view.downloader.requestDownload(tile.img, tile.url, this.hips.useCors);
+                this.hips.lastUpdateDateNeededTiles = now;
+                view.requestRedrawAtDate(now+HpxImageSurvey.UPDATE_NEEDED_TILES_DELAY+10);
+            }
+
+
+            return n;
+        },
+
+        drawChildren: function(ctx, view, maxParente) {
+            var n=0;
+            var limitOrder = 13; // corresponds to NSIDE=8192, current HealpixJS limit
+            if ( this.width>1 && this.norder<limitOrder && this.parente<maxParente ) {
+                var children = this.getChildren();
+                if ( children!=null ) {
+                    for ( var i=0; i<4; i++ ) {
+//console.log(i);
+                        if ( children[i]!=null ) {
+                            n += children[i].draw(ctx , view, maxParente);
+                        }
+                    }
+                }
+            }
+
+            return n;
+        },
+
+
+        // returns the 4 HpxKey children
+        getChildren: function() {
+            if (this.children!=null) {
+                return this.children;
+            }
+
+            var children = [];
+            for ( var childNb=0; childNb<4; childNb++ ) {
+                var child = HpxKey.createHpxKeyfromAncestor(this, childNb);
+                children[childNb] = child;
+            }
+            this.children = children;
+
+
+            return this.children;
+        },
+
+
+
+        getProjViewCorners: function(view) {
+            var cornersXY = [];
+            var cornersXYView = [];
+            var spVec = new SpatialVector();
+
+            corners = HealpixCache.corners_nest(this.npix, this.nside);
+
+            var lon, lat;
+            for (var k=0; k<4; k++) {
+                spVec.setXYZ(corners[k].x, corners[k].y, corners[k].z);
+
+                // need for frame transformation ?
+                if (this.frame.system != view.cooFrame.system) {
+                    if (this.frame.system == CooFrameEnum.SYSTEMS.J2000) {
+                        var radec = CooConversion.J2000ToGalactic([spVec.ra(), spVec.dec()]);
+                        lon = radec[0];
+                        lat = radec[1];
+                    }
+                    else if (this.frame.system == CooFrameEnum.SYSTEMS.GAL) {
+                        var radec = CooConversion.GalacticToJ2000([spVec.ra(), spVec.dec()]);
+                        lon = radec[0];
+                        lat = radec[1];
+                    }
+                }
+                else {
+                    lon = spVec.ra();
+                    lat = spVec.dec();
+                }
+                cornersXY[k] = view.projection.project(lon, lat);
+            }
+
+
+            if (cornersXY[0] == null ||  cornersXY[1] == null  ||  cornersXY[2] == null ||  cornersXY[3] == null ) {
+                return null;
+            }
+
+
+
+            for (var k=0; k<4; k++) {
+                cornersXYView[k] = AladinUtils.xyToView(cornersXY[k].X, cornersXY[k].Y, view.width, view.height, view.largestDim, view.zoomFactor);
+            }
+
+            return cornersXYView;
+        }
+
+    } // end of HpxKey.prototype
+
+
+
+
+    /** Returns the squared distance for points in array c at indexes g and d
+     */
+    var dist = function(c, g, d) {
+        var dx=c[g].vx-c[d].vx;
+        var dy=c[g].vy-c[d].vy;
+        return  dx*dx + dy*dy;
+    }
+
+
+    var M = 280*280;
+    var N = 150*150;
+    var RAP=0.7;
+
+    /** Returns true if the HEALPix rhomb described by its 4 corners (array c)
+     * is too large to be drawn in one pass ==> need to be subdivided */
+    var isTooLarge = function(c) {
+
+        var d1,d2;
+        if ( (d1=dist(c,0,2))>M || (d2=dist(c,2,1))>M ) {
+            return true;
+        }
+        if ( d1==0 || d2==0 ) {
+            throw "Rhomb error";
+        }
+        var diag1 = dist(c,0,3);
+        var diag2 = dist(c,1,2);
+        if ( diag2==0 || diag2==0 ) {
+            throw "Rhomb error";
+        }
+        var rap = diag2>diag1 ? diag1/diag2 : diag2/diag1;
+
+        return rap<RAP && (diag1>N || diag2>N);
+    }
+
+
+    return HpxKey;
+
+})();
+
+
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File HpxImageSurvey
+ *
+ * Author: Thomas Boch [CDS]
+ *
+ *****************************************************************************/
+
+HpxImageSurvey = (function() {
+
+
+    /** Constructor
+     * cooFrame and maxOrder can be set to null
+     * They will be determined by reading the properties file
+     *
+     */
+    var HpxImageSurvey = function(idOrHiPSDefinition, name, rootUrl, cooFrame, maxOrder, options) {
+        // new way
+        if (idOrHiPSDefinition instanceof HiPSDefinition) {
+            this.hipsDefinition = idOrHiPSDefinition;
+
+        }
+
+        else {
+// REPRENDRE LA,  EN CREANT l'OBJET HiPSDefinition ou FAIRE dans l'autre sens
+            // old way, we retrofit parameters into a HiPSDefinition object
+            var hipsDefProps = {};
+
+            this.id = idOrHiPSDefinition;
+            hipsDefProps['ID'] = this.id;
+
+    	    this.name = name;
+            hipsDefProps['obs_title'] = this.name;
+
+            // remove final slash
+    	    if (rootUrl.slice(-1) === '/') {
+    	        this.rootUrl = rootUrl.substr(0, rootUrl.length-1);
+    	    }
+    	    else {
+    	        this.rootUrl = rootUrl;
+    	    }
+            this.additionalParams = (options && options.additionalParams) || null; // parameters for cut, stretch, etc
+
+            // make URL absolute
+            this.rootUrl = Utils.getAbsoluteURL(this.rootUrl);
+
+            // fast fix for HTTPS support --> will work for all HiPS served by CDS
+            if (Utils.isHttpsContext() && ( /u-strasbg.fr/i.test(this.rootUrl) || /unistra.fr/i.test(this.rootUrl)  ) ) {
+                this.rootUrl = this.rootUrl.replace('http://', 'https://');
+            }
+
+            // temporary fix when alasky is under maintenance
+            //this.rootUrl = this.rootUrl.replace('alasky.', 'alaskybis.');
+
+    	    options = options || {};
+    	    // TODO : support PNG
+    	    this.imgFormat = options.imgFormat || 'jpg';
+
+            // permet de forcer l'affichage d'un certain niveau
+            this.minOrder = options.minOrder || null;
+
+
+            // TODO : lire depuis fichier properties
+            this.cooFrame = CooFrameEnum.fromString(cooFrame, CooFrameEnum.J2000);
+
+            this.longitudeReversed = options.longitudeReversed || false;
+
+            // force coo frame for Glimpse 360
+            if (this.rootUrl.indexOf('/glimpse360/aladin/data')>=0) {
+                this.cooFrame = CooFrameEnum.J2000;
+            }
+            // TODO : lire depuis fichier properties
+            this.maxOrder = maxOrder;
+
+            this.hipsDefinition = HiPSDefinition.fromProperties(hipsDefProps);
+        }
+
+        this.ascendingLongitude = false;
+
+        this.tileSize = undefined;
+    	this.allskyTexture = null;
+    	this.alpha = 0.0; // opacity value between 0 and 1 (if this layer is an opacity layer)
+    	this.allskyTextureSize = 0;
+        this.lastUpdateDateNeededTiles = 0;
+
+        var found = false;
+        for (var k=0; k<HpxImageSurvey.SURVEYS.length; k++) {
+            if (HpxImageSurvey.SURVEYS[k].id==this.id) {
+                found = true;
+            }
+        }
+        if (! found) {
+            HpxImageSurvey.SURVEYS.push({
+                 "id": this.id,
+                 "url": this.rootUrl,
+                 "name": this.name,
+                 "maxOrder": this.maxOrder,
+                 "frame": this.cooFrame
+            });
+        }
+        HpxImageSurvey.SURVEYS_OBJECTS[this.id] = this;
+    };
+
+
+
+    HpxImageSurvey.UPDATE_NEEDED_TILES_DELAY = 1000; // in milliseconds
+
+    HpxImageSurvey.prototype.init = function(view, callback) {
+    	this.view = view;
+
+        if (!this.cm) {
+            this.cm = new ColorMap(this.view);
+        }
+
+    	// tileBuffer is now shared across different image surveys
+    	//this.tileBuffer = new TileBuffer();
+    	this.tileBuffer = this.view.tileBuffer;
+
+    	this.useCors = false;
+    	var self = this;
+        if ($.support.cors) {
+            // testing if server supports CORS ( http://www.html5rocks.com/en/tutorials/cors/ )
+            $.ajax({
+                type: 'GET',
+                url: this.rootUrl + '/properties'  + (this.additionalParams ? ('?' + this.additionalParams) : ''),
+                dataType: 'text',
+                xhrFields: {
+                },
+                headers: {
+                },
+                success: function() {
+                    // CORS is supported
+                    self.useCors = true;
+
+                    self.retrieveAllskyTextures();
+                    if (callback) {
+                        callback();
+                    }
+                },
+                error: function(jqXHR, textStatus, errorThrown) {
+                    // CORS is not supported
+                    self.retrieveAllskyTextures();
+                    if (callback) {
+                        callback();
+                    }
+                }
+              });
+        }
+        else {
+            this.retrieveAllskyTextures();
+            callback();
+        }
+
+    };
+
+    HpxImageSurvey.DEFAULT_SURVEY_ID = "P/DSS2/color";
+
+    HpxImageSurvey.SURVEYS_OBJECTS = {};
+    HpxImageSurvey.SURVEYS = [
+     {
+        "id": "P/2MASS/color",
+        "url": "http://alasky.u-strasbg.fr/2MASS/Color",
+        "name": "2MASS colored",
+        "maxOrder": 9,
+        "frame": "equatorial",
+        "format": "jpeg"
+     },
+     {
+        "id": "P/DSS2/color",
+        "url": "http://alasky.u-strasbg.fr/DSS/DSSColor",
+        "name": "DSS colored",
+        "maxOrder": 9,
+        "frame": "equatorial",
+        "format": "jpeg"
+     },
+     {
+        "id": "P/DSS2/red",
+        "url": "http://alasky.u-strasbg.fr/DSS/DSS2Merged",
+        "name": "DSS2 Red (F+R)",
+        "maxOrder": 9,
+        "frame": "equatorial",
+        "format": "jpeg fits"
+     },
+     {
+        "id": "P/PanSTARRS/DR1/g",
+        "url": "http://alasky.u-strasbg.fr/Pan-STARRS/DR1/g",
+        "name": "PanSTARRS DR1 g",
+        "maxOrder": 11,
+        "frame": "equatorial",
+        "format": "jpeg fits"
+     },
+     {
+        "id": "P/PanSTARRS/DR1/color-z-zg-g",
+        "url": "http://alasky.u-strasbg.fr/Pan-STARRS/DR1/color-z-zg-g",
+        "name": "PanSTARRS DR1 color",
+        "maxOrder": 11,
+        "frame": "equatorial",
+        "format": "jpeg"
+     },
+     {
+        "id": "P/DECaPS/DR1/color",
+        "url": "http://alasky.u-strasbg.fr/DECaPS/DR1/color",
+        "name": "DECaPS DR1 color",
+        "maxOrder": 11,
+        "frame": "equatorial",
+        "format": "jpeg png"
+     },
+     {
+        "id": "P/Fermi/color",
+        "url": "http://alasky.u-strasbg.fr/Fermi/Color",
+        "name": "Fermi color",
+        "maxOrder": 3,
+        "frame": "equatorial",
+        "format": "jpeg"
+     },
+     {
+        "id": "P/Finkbeiner",
+        "url": "http://alasky.u-strasbg.fr/FinkbeinerHalpha",
+        "maxOrder": 3,
+        "frame": "galactic",
+        "format": "jpeg fits",
+        "name": "Halpha"
+     },
+     {
+        "id": "P/GALEXGR6/AIS/color",
+        "url": "http://alasky.unistra.fr/GALEX/GR6-03-2014/AIS-Color",
+        "name": "GALEX Allsky Imaging Survey colored",
+        "maxOrder": 8,
+        "frame": "equatorial",
+        "format": "jpeg"
+     },
+     {
+        "id": "P/IRIS/color",
+        "url": "http://alasky.u-strasbg.fr/IRISColor",
+        "name": "IRIS colored",
+        "maxOrder": 3,
+        "frame": "galactic",
+        "format": "jpeg"
+     },
+     {
+        "id": "P/Mellinger/color",
+        "url": "http://alasky.u-strasbg.fr/MellingerRGB",
+        "name": "Mellinger colored",
+        "maxOrder": 4,
+        "frame": "galactic",
+        "format": "jpeg"
+     },
+     {
+        "id": "P/SDSS9/color",
+        "url": "http://alasky.u-strasbg.fr/SDSS/DR9/color",
+        "name": "SDSS9 colored",
+        "maxOrder": 10,
+        "frame": "equatorial",
+        "format": "jpeg"
+     },
+     {
+        "id": "P/SPITZER/color",
+        "url": "http://alasky.u-strasbg.fr/SpitzerI1I2I4color",
+        "name": "IRAC color I1,I2,I4 - (GLIMPSE, SAGE, SAGE-SMC, SINGS)",
+        "maxOrder": 9,
+        "frame": "galactic",
+        "format": "jpeg"
+     },
+     {
+        "id": "P/VTSS/Ha",
+        "url": "http://alasky.u-strasbg.fr/VTSS/Ha",
+        "maxOrder": 3,
+        "frame": "galactic",
+        "format": "png jpeg fits",
+        "name": "VTSS-Ha"
+     },
+     {
+        "id": "P/XMM/EPIC",
+        "url": "http://saada.u-strasbg.fr/xmmallsky",
+        "name": "XMM-Newton stacked EPIC images (no phot. normalization)",
+        "maxOrder": 7,
+        "frame": "equatorial",
+        "format": "png fits"
+     },
+     {
+         "id": "P/XMM/PN/color",
+          "url": "http://saada.unistra.fr/PNColor",
+          "name": "XMM PN colored",
+          "maxOrder": 7,
+          "frame": "equatorial",
+          "format": "png jpeg"
+     },
+     {
+         "id": "P/allWISE/color",
+         "url": "http://alasky.u-strasbg.fr/AllWISE/RGB-W4-W2-W1/",
+         "name": "AllWISE color",
+         "maxOrder": 8,
+         "frame": "equatorial",
+         "format": "jpeg"
+     },
+     {
+         "id": "P/GLIMPSE360",
+         "url": "http://www.spitzer.caltech.edu/glimpse360/aladin/data",
+         "name": "GLIMPSE360",
+         "maxOrder": 9,
+         "frame": "equatorial",
+         "format": "jpeg"
+     }
+  ];
+
+
+
+    HpxImageSurvey.getAvailableSurveys = function() {
+    	return HpxImageSurvey.SURVEYS;
+    };
+
+    HpxImageSurvey.getSurveyInfoFromId = function(id) {
+        var surveys = HpxImageSurvey.getAvailableSurveys();
+        for (var i=0; i<surveys.length; i++) {
+            if (surveys[i].id==id) {
+                return surveys[i];
+            }
+        }
+        return null;
+    };
+
+    HpxImageSurvey.getSurveyFromId = function(id) {
+        if (HpxImageSurvey.SURVEYS_OBJECTS[id]) {
+            return HpxImageSurvey.SURVEYS_OBJECTS[id];
+        }
+        var surveyInfo = HpxImageSurvey.getSurveyInfoFromId(id);
+        if (surveyInfo) {
+            var options = {};
+            if ( surveyInfo.format && surveyInfo.format.indexOf('jpeg')<0 && surveyInfo.format.indexOf('png')>=0 ) {
+                options.imgFormat = 'png';
+            }
+            return new HpxImageSurvey(surveyInfo.id, surveyInfo.name, surveyInfo.url, surveyInfo.frame, surveyInfo.maxOrder, options);
+        }
+
+        return null;
+    }
+
+
+    HpxImageSurvey.prototype.getTileURL = function(norder, npix) {
+    	var dirIdx = Math.floor(npix/10000)*10000;
+    	return this.rootUrl + "/" + "Norder" + norder + "/Dir" + dirIdx + "/Npix" + npix + "." + this.imgFormat  + (this.additionalParams ? ('?' + this.additionalParams) : '');;
+    };
+
+    HpxImageSurvey.prototype.retrieveAllskyTextures = function() {
+    	// start loading of allsky
+    	var img = new Image();
+    	if (this.useCors) {
+            img.crossOrigin = 'anonymous';
+        }
+    	var self = this;
+    	img.onload = function() {
+    		// sur ipad, le fichier qu'on récupère est 2 fois plus petit. Il faut donc déterminer la taille de la texture dynamiquement
+    	    self.allskyTextureSize = img.width/27;
+            self.allskyTexture = img;
+
+            /*
+    		// récupération des 768 textures (NSIDE=4)
+    		for (var j=0; j<29; j++) {
+    			for (var i=0; i<27; i++) {
+    				var c = document.createElement('canvas');
+    				c.width = c.height = self.allskyTextureSize;
+    				c.allSkyTexture = true;
+    				var context = c.getContext('2d');
+    				context.drawImage(img, i*self.allskyTextureSize, j*self.allskyTextureSize, self.allskyTextureSize, self.allskyTextureSize, 0, 0, c.width, c.height);
+    				self.allskyTextures.push(c);
+    			}
+    		}
+            */
+    		self.view.requestRedraw();
+    	};
+    	img.src = this.rootUrl + '/Norder3/Allsky.' + this.imgFormat + (this.additionalParams ? ('?' + this.additionalParams) : '');
+
+    };
+
+    // Nouvelle méthode pour traitement des DEFORMATIONS
+    /**
+     * Draw the image survey according
+     *
+     * @param ctx: canvas context where to draw
+     * @param view
+     * @param subdivide: should
+     *
+     */
+    HpxImageSurvey.prototype.draw = function(ctx, view, subdivide, curOverlayNorder) {
+        subdivide = (subdivide===undefined) ? false: subdivide;
+
+        var cornersXYViewMapAllsky = view.getVisibleCells(3, this.cooFrame);
+        var cornersXYViewMapHighres = null;
+
+
+
+        var norder4Display = Math.min(curOverlayNorder, this.maxOrder);
+        if (curOverlayNorder>=3) {
+            if (curOverlayNorder==3) {
+                cornersXYViewMapHighres = cornersXYViewMapAllsky;
+            }
+            else {
+                cornersXYViewMapHighres = view.getVisibleCells(norder4Display, this.cooFrame);
+            }
+        }
+
+        // new way of drawing
+        if (subdivide) {
+
+            if (curOverlayNorder<=4) {
+                this.drawAllsky(ctx, cornersXYViewMapAllsky, norder4Display, view);
+            }
+
+            if (curOverlayNorder>=3) {
+                this.drawHighres(ctx, cornersXYViewMapHighres, norder4Display, view);
+            }
+/*
+            else {
+                this.drawAllsky(ctx, cornersXYViewMapAllsky, norder4Display, view);
+            }
+*/
+
+            return;
+        }
+
+        // regular way of drawing
+        // TODO : a t on besoin de dessiner le allsky si norder>=3 ?
+        // TODO refactoring : devrait être une méthode de HpxImageSurvey
+        if (view.curNorder>=3) {
+            this.redrawHighres(ctx, cornersXYViewMapHighres, view.curNorder);
+        }
+        else {
+            this.redrawAllsky(ctx, cornersXYViewMapAllsky, view.fov, view.curNorder);
+        }
+
+    };
+
+    HpxImageSurvey.prototype.drawHighres = function(ctx, cornersXYViewMap, norder, view) {
+//////////////////////////////
+        var parentTilesToDraw = [];
+        var parentTilesToDrawIndex = {};
+        var parentTilesMissingIndex = {};
+        for (var k=0; k<cornersXYViewMap.length; k++) {
+            var ipix = cornersXYViewMap[k].ipix
+            var tileURL = this.getTileURL(norder, ipix);
+            var tile = this.tileBuffer.getTile(tileURL);
+            var tileAvailable = tile && Tile.isImageOk(tile.img);
+            if (! tileAvailable) { // if tile is not available, search if upper level tiles can be drawn
+                var MAX_UPPER_LEVELS = 4; // we search parent tiles up to 4 levels
+                for (var parentOrder = norder -1 ; parentOrder>=3 && parentOrder >= norder-MAX_UPPER_LEVELS ; parentOrder--) {
+                    var parentIpix = ~~(ipix / Math.pow(4, norder - parentOrder));
+                    var key = parentOrder + '-' + parentIpix;
+                    if (parentTilesToDrawIndex[key]===true || parentTilesMissingIndex===true) {
+                        break;
+                    }
+                    var parentTileURL = this.getTileURL(parentOrder, parentIpix);
+                    var parentTile = this.tileBuffer.getTile(parentTileURL);
+                    var parentTileAvailable = parentTile && Tile.isImageOk(parentTile.img);
+                    if (parentTileAvailable) {
+                        parentTilesToDraw.push({ipix: parentIpix, order: parentOrder});
+                        parentTilesToDrawIndex[key] = true;
+
+                        break;
+                    }
+                    else {
+                        parentTilesMissingIndex[key] = true;
+                    }
+                }
+            }
+        }
+        // sort to draw lower norder first
+        parentTilesToDraw = parentTilesToDraw.sort(function(itemA, itemB) {
+            return itemA.order - itemB.order;
+        });
+
+//////////////////////////////
+
+        var tSize = this.tileSize || 512;
+        // draw parent tiles
+        for (var k=0; k<parentTilesToDraw.length; k++) {
+            var t = parentTilesToDraw[k];
+            new HpxKey(t.order, t.ipix, this, tSize, tSize).draw(ctx, view);
+        }
+
+        // TODO : we could have a pool of HpxKey to prevent object re-creation at each frame
+        // draw tiles
+        for (var k=0; k<cornersXYViewMap.length; k++) {
+            new HpxKey(norder, cornersXYViewMap[k].ipix, this, tSize, tSize).draw(ctx, view);
+        }
+    };
+
+    HpxImageSurvey.prototype.drawAllsky = function(ctx, cornersXYViewMap, norder, view) {
+        // for norder deeper than 6, we think it brings nothing to draw the all-sky
+        if (this.view.curNorder>6) {
+            return;
+        }
+
+        if ( ! this.allskyTexture || !Tile.isImageOk(this.allskyTexture) ) {
+            return;
+        }
+
+        var hpxKeys = [];
+    	var cornersXYView;
+        var ipix;
+        var dx, dy;
+        for (var k=0; k<cornersXYViewMap.length; k++) {
+    		cornersXYView = cornersXYViewMap[k];
+    		ipix = cornersXYView.ipix;
+            dy = this.allskyTextureSize * Math.floor(ipix/27);
+            dx = this.allskyTextureSize * (ipix - 27*Math.floor(ipix/27));
+            hpxKeys.push(new HpxKey(3, cornersXYViewMap[k].ipix, this, this.allskyTextureSize, this.allskyTextureSize, dx, dy, this.allskyTexture, this.allskyTextureSize));
+        }
+
+        for (var k=0; k<hpxKeys.length; k++) {
+            hpxKeys[k].draw(ctx, view);
+        }
+    };
+
+
+    HpxImageSurvey.prototype.redrawAllsky = function(ctx, cornersXYViewMap, fov, norder) {
+    	// for norder deeper than 6, we think it brings nothing to draw the all-sky
+    	if (this.view.curNorder>6) {
+    		return;
+    	}
+
+    	if ( ! this.allskyTexture ) {
+    		return;
+    	}
+
+
+    	var cornersXYView;
+        var coeff = 0;
+        var center;
+        var ipix;
+    	for (var k=0, len=cornersXYViewMap.length; k<len; k++) {
+    		cornersXYView = cornersXYViewMap[k];
+    		ipix = cornersXYView.ipix;
+
+
+
+            if ( ! this.allskyTexture || !Tile.isImageOk(this.allskyTexture) ) {
+                continue;
+            }
+
+            var dy = this.allskyTextureSize * Math.floor(ipix/27);
+            var dx = this.allskyTextureSize * (ipix - 27*Math.floor(ipix/27));
+
+
+
+    		// TODO : plutot agrandir le clip ?
+    	    // grow cornersXYView
+    	    if (fov>40) {
+    			coeff = 0.02;
+                coeff = 0.0;
+    	        center = {x: (cornersXYView[0].vx+cornersXYView[2].vx)/2, y: (cornersXYView[0].vy+cornersXYView[2].vy)/2};
+    	        for (var i=0; i<4; i++) {
+    	            var diff = {x: cornersXYView[i].vx-center.x, y: cornersXYView[i].vy-center.y};
+    	            cornersXYView[i].vx += coeff*diff.x;
+    	            cornersXYView[i].vy += coeff*diff.y;
+    	        }
+    	    }
+
+    	    this.drawOneTile(ctx, this.allskyTexture, cornersXYView, this.allskyTextureSize, null, dx, dy, true);
+    	}
+    };
+
+    HpxImageSurvey.prototype.getColorMap = function() {
+        return this.cm;
+    };
+
+    var drawEven = true;
+    // TODO: avoir un mode où on ne cherche pas à dessiner d'abord les tuiles parentes (pour génération vignettes côté serveur)
+    HpxImageSurvey.prototype.redrawHighres = function(ctx, cornersXYViewMap, norder) {
+
+        // DOES THAT FIX THE PROBLEM ???
+        if (cornersXYViewMap.length==0) {
+            return;
+        }
+
+        drawEven = ! drawEven;
+        var now = new Date().getTime();
+        var updateNeededTiles = (now-this.lastUpdateDateNeededTiles) > HpxImageSurvey.UPDATE_NEEDED_TILES_DELAY;
+        var tile, url, parentTile, parentUrl;
+        var parentNorder = norder - 1;
+        var cornersXYView, parentCornersXYView;
+        var tilesToDraw = [];
+        var parentTilesToDraw = [];
+        var parentTilesToDrawIpix = {};
+        var missingTiles = false;
+
+        var tilesToDownload = [];
+        var parentTilesToDownload = [];
+
+        var parentIpix;
+        var ipix;
+
+        // tri des tuiles selon la distance
+        if (updateNeededTiles) {
+            var center = [(cornersXYViewMap[0][0].vx+cornersXYViewMap[0][1].vx)/2, (cornersXYViewMap[0][0].vy+cornersXYViewMap[0][1].vy)/2];
+            var newCornersXYViewMap = cornersXYViewMap.sort(function(a, b) {
+                var cA = [(a[0].vx+a[2].vx)/2, (a[0].vy+a[2].vy)/2];
+                var cB = [(b[0].vx+b[2].vx)/2, (b[0].vy+b[2].vy)/2];
+
+                var distA = (cA[0]-center[0])*(cA[0]-center[0]) + (cA[1]-center[1])*(cA[1]-center[1]);
+                var distB = (cB[0]-center[0])*(cB[0]-center[0]) + (cB[1]-center[1])*(cB[1]-center[1]);
+
+                return distA-distB;
+
+            });
+            cornersXYViewMap = newCornersXYViewMap;
+        }
+
+
+    	for (var k=0, len=cornersXYViewMap.length; k<len; k++) {
+    		cornersXYView = cornersXYViewMap[k];
+    		ipix = cornersXYView.ipix;
+
+            // on demande à charger le parent (cas d'un zoomOut)
+            // TODO : mettre priorité plus basse
+            parentIpix = ~~(ipix/4);
+        	parentUrl = this.getTileURL(parentNorder, parentIpix);
+            if (updateNeededTiles && parentNorder>=3) {
+            	parentTile = this.tileBuffer.addTile(parentUrl);
+                if (parentTile) {
+                    parentTilesToDownload.push({img: parentTile.img, url: parentUrl});
+                }
+            }
+
+            url = this.getTileURL(norder, ipix);
+            tile = this.tileBuffer.getTile(url);
+
+            if ( ! tile ) {
+                missingTiles = true;
+
+                if (updateNeededTiles) {
+                    var tile = this.tileBuffer.addTile(url);
+                    if (tile) {
+                        tilesToDownload.push({img: tile.img, url: url});
+                    }
+                }
+
+                // is the parent tile available ?
+                if (parentNorder>=3 && ! parentTilesToDrawIpix[parentIpix]) {
+                	parentTile = this.tileBuffer.getTile(parentUrl);
+                	if (parentTile && Tile.isImageOk(parentTile.img)) {
+                		parentCornersXYView = this.view.getPositionsInView(parentIpix, parentNorder);
+                		if (parentCornersXYView) {
+                			parentTilesToDraw.push({img: parentTile.img, corners: parentCornersXYView, ipix: parentIpix});
+                		}
+                	}
+                	parentTilesToDrawIpix[parentIpix] = 1;
+                }
+
+                continue;
+            }
+            else if ( ! Tile.isImageOk(tile.img)) {
+                missingTiles = true;
+                if (updateNeededTiles && ! tile.img.dlError) {
+                    tilesToDownload.push({img: tile.img, url: url});
+                }
+
+                // is the parent tile available ?
+                if (parentNorder>=3 && ! parentTilesToDrawIpix[parentIpix]) {
+                	parentTile = this.tileBuffer.getTile(parentUrl);
+                	if (parentTile && Tile.isImageOk(parentTile.img)) {
+                		parentCornersXYView = this.view.getPositionsInView(parentIpix, parentNorder);
+                		if (parentCornersXYView) {
+                			parentTilesToDraw.push({img: parentTile.img, corners: parentCornersXYView, ipix: parentIpix});
+                		}
+                	}
+                	parentTilesToDrawIpix[parentIpix] = 1;
+                }
+
+                continue;
+            }
+            tilesToDraw.push({img: tile.img, corners: cornersXYView});
+        }
+
+
+
+        // draw parent tiles
+        for (var k=0, len = parentTilesToDraw.length; k<len; k++) {
+        	this.drawOneTile(ctx, parentTilesToDraw[k].img, parentTilesToDraw[k].corners, parentTilesToDraw[k].img.width);
+        }
+
+        // draw tiles
+        ///*
+        for (var k=0, len = tilesToDraw.length; k<len; k++) {
+        	var alpha = null;
+        	var img = tilesToDraw[k].img;
+        	if (img.fadingStart) {
+        		if (img.fadingEnd && now<img.fadingEnd) {
+        			alpha = 0.2 + (now - img.fadingStart)/(img.fadingEnd - img.fadingStart)*0.8;
+                    this.requestRedraw();
+        		}
+        	}
+        	this.drawOneTile(ctx, img, tilesToDraw[k].corners, img.width, alpha);
+        }
+        //*/
+
+
+        // demande de chargement des tuiles manquantes et mise à jour lastUpdateDateNeededTiles
+        if (updateNeededTiles) {
+            // demande de chargement des tuiles
+            for (var k=0, len = tilesToDownload.length; k<len; k++) {
+                this.view.downloader.requestDownload(tilesToDownload[k].img, tilesToDownload[k].url, this.useCors);
+            }
+            //demande de chargement des tuiles parentes
+            for (var k=0, len = parentTilesToDownload.length; k<len; k++) {
+                this.view.downloader.requestDownload(parentTilesToDownload[k].img, parentTilesToDownload[k].url, this.useCors);
+            }
+            this.lastUpdateDateNeededTiles = now;
+        }
+        if (missingTiles) {
+            // callback pour redemander un display dans 1000ms
+            this.view.requestRedrawAtDate(now+HpxImageSurvey.UPDATE_NEEDED_TILES_DELAY+10);
+        }
+    };
+
+    function dist2(x1,y1,x2,y2) {
+    	return Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2);
+    }
+
+    HpxImageSurvey.prototype.drawOneTile = function(ctx, img, cornersXYView, textureSize, alpha, dx, dy, applyCorrection) {
+
+        // apply CM
+        var newImg = this.useCors ? this.cm.apply(img) : img;
+
+
+    	// is the tile a diamond ?
+    //	var round = AladinUtils.myRound;
+    //	var b = cornersXYView;
+    //	var flagDiamond =  round(b[0].vx - b[2].vx) == round(b[1].vx - b[3].vx)
+    //    				&& round(b[0].vy - b[2].vy) == round(b[1].vy - b[3].vy);
+
+    	drawTexturedTriangle(ctx, newImg,
+                cornersXYView[0].vx, cornersXYView[0].vy,
+                cornersXYView[1].vx, cornersXYView[1].vy,
+    	        cornersXYView[3].vx, cornersXYView[3].vy,
+    	        textureSize-1, textureSize-1,
+    	        textureSize-1, 0,
+    	        0, textureSize-1,
+    	        alpha,
+                dx, dy, applyCorrection);
+        drawTexturedTriangle(ctx, newImg,
+        		cornersXYView[1].vx, cornersXYView[1].vy,
+        		cornersXYView[3].vx, cornersXYView[3].vy,
+        		cornersXYView[2].vx, cornersXYView[2].vy,
+        		textureSize-1, 0,
+        		0, textureSize-1,
+        		0, 0,
+        		alpha,
+                dx, dy, applyCorrection);
+    };
+
+       HpxImageSurvey.prototype.drawOneTile2 = function(ctx, img, cornersXYView, textureSize, alpha, dx, dy, applyCorrection, norder) {
+
+        // apply CM
+        var newImg = this.useCors ? this.cm.apply(img) : img;
+
+
+        // is the tile a diamond ?
+    //  var round = AladinUtils.myRound;
+    //  var b = cornersXYView;
+    //  var flagDiamond =  round(b[0].vx - b[2].vx) == round(b[1].vx - b[3].vx)
+    //                  && round(b[0].vy - b[2].vy) == round(b[1].vy - b[3].vy);
+
+        var delta = norder<=3 ? (textureSize<100 ? 0.5 : 0.2) : 0;
+        drawTexturedTriangle2(ctx, newImg,
+                cornersXYView[0].vx, cornersXYView[0].vy,
+                cornersXYView[1].vx, cornersXYView[1].vy,
+                cornersXYView[3].vx, cornersXYView[3].vy,
+                textureSize-delta, textureSize-delta,
+                textureSize-delta, 0+delta,
+                0+delta, textureSize-delta,
+                alpha,
+                dx, dy, applyCorrection, norder);
+        drawTexturedTriangle2(ctx, newImg,
+                cornersXYView[1].vx, cornersXYView[1].vy,
+                cornersXYView[3].vx, cornersXYView[3].vy,
+                cornersXYView[2].vx, cornersXYView[2].vy,
+                textureSize-delta, 0+delta,
+                0+delta, textureSize-delta,
+                0+delta, 0+delta,
+                alpha,
+                dx, dy, applyCorrection, norder);
+    };
+
+    function drawTexturedTriangle2(ctx, img, x0, y0, x1, y1, x2, y2,
+                                        u0, v0, u1, v1, u2, v2, alpha,
+                                        dx, dy, applyCorrection, norder) {
+
+        dx = dx || 0;
+        dy = dy || 0;
+
+        if (!applyCorrection) {
+            applyCorrection = false;
+        }
+
+        u0 += dx;
+        u1 += dx;
+        u2 += dx;
+        v0 += dy;
+        v1 += dy;
+        v2 += dy;
+        var xc = (x0 + x1 + x2) / 3;
+        var yc = (y0 + y1 + y2) / 3;
+
+
+        // ---- centroid ----
+        var xc = (x0 + x1 + x2) / 3;
+        var yc = (y0 + y1 + y2) / 3;
+        ctx.save();
+        if (alpha) {
+            ctx.globalAlpha = alpha;
+        }
+
+/*
+        var coeff = 0.01; // default value
+        if (applyCorrection) {
+            coeff = 0.01;
+        }
+        if (norder<3) {
+            coeff = 0.02; // TODO ????
+        }
+*/
+coeff = 0.02;
+
+        // ---- scale triangle by (1 + coeff) to remove anti-aliasing and draw ----
+        ctx.beginPath();
+        ctx.moveTo(((1+coeff) * x0 - xc * coeff), ((1+coeff) * y0 - yc * coeff));
+        ctx.lineTo(((1+coeff) * x1 - xc * coeff), ((1+coeff) * y1 - yc * coeff));
+        ctx.lineTo(((1+coeff) * x2 - xc * coeff), ((1+coeff) * y2 - yc * coeff));
+        ctx.closePath();
+        ctx.clip();
+
+        // this is needed to prevent to see some lines between triangles
+        if (applyCorrection) {
+            coeff = 0.01;
+            x0 = ((1+coeff) * x0 - xc * coeff), y0 = ((1+coeff) * y0 - yc * coeff);
+            x1 = ((1+coeff) * x1 - xc * coeff), y1 = ((1+coeff) * y1 - yc * coeff);
+            x2 = ((1+coeff) * x2 - xc * coeff), y2 = ((1+coeff) * y2 - yc * coeff);
+        }
+
+        // ---- transform texture ----
+        var d_inv = 1/ (u0 * (v2 - v1) - u1 * v2 + u2 * v1 + (u1 - u2) * v0);
+        ctx.transform(
+            -(v0 * (x2 - x1) -  v1 * x2  + v2 *  x1 + (v1 - v2) * x0) * d_inv, // m11
+             (v1 *  y2 + v0  * (y1 - y2) - v2 *  y1 + (v2 - v1) * y0) * d_inv, // m12
+             (u0 * (x2 - x1) -  u1 * x2  + u2 *  x1 + (u1 - u2) * x0) * d_inv, // m21
+            -(u1 *  y2 + u0  * (y1 - y2) - u2 *  y1 + (u2 - u1) * y0) * d_inv, // m22
+             (u0 * (v2 * x1  -  v1 * x2) + v0 * (u1 *  x2 - u2  * x1) + (u2 * v1 - u1 * v2) * x0) * d_inv, // dx
+             (u0 * (v2 * y1  -  v1 * y2) + v0 * (u1 *  y2 - u2  * y1) + (u2 * v1 - u1 * v2) * y0) * d_inv  // dy
+        );
+        ctx.drawImage(img, 0, 0);
+        //ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);
+
+    //    ctx.globalAlpha = 1.0;
+
+        ctx.restore();
+    }
+
+
+    // uses affine texture mapping to draw a textured triangle
+    // at screen coordinates [x0, y0], [x1, y1], [x2, y2] from
+    // img *pixel* coordinates [u0, v0], [u1, v1], [u2, v2]
+    // code from http://www.dhteumeuleu.com/lab/image3D.html
+    function drawTexturedTriangle(ctx, img, x0, y0, x1, y1, x2, y2,
+                                        u0, v0, u1, v1, u2, v2, alpha,
+                                        dx, dy, applyCorrection) {
+
+        dx = dx || 0;
+        dy = dy || 0;
+
+        if (!applyCorrection) {
+            applyCorrection = false;
+        }
+
+        u0 += dx;
+        u1 += dx;
+        u2 += dx;
+        v0 += dy;
+        v1 += dy;
+        v2 += dy;
+        var xc = (x0 + x1 + x2) / 3;
+        var yc = (y0 + y1 + y2) / 3;
+
+
+        // ---- centroid ----
+        var xc = (x0 + x1 + x2) / 3;
+        var yc = (y0 + y1 + y2) / 3;
+        ctx.save();
+        if (alpha) {
+        	ctx.globalAlpha = alpha;
+        }
+
+        var coeff = 0.01; // default value
+        if (applyCorrection) {
+            coeff = 0.01;
+        }
+        // ---- scale triangle by (1 + coeff) to remove anti-aliasing and draw ----
+        ctx.beginPath();
+        ctx.moveTo(((1+coeff) * x0 - xc * coeff), ((1+coeff) * y0 - yc * coeff));
+        ctx.lineTo(((1+coeff) * x1 - xc * coeff), ((1+coeff) * y1 - yc * coeff));
+        ctx.lineTo(((1+coeff) * x2 - xc * coeff), ((1+coeff) * y2 - yc * coeff));
+        ctx.closePath();
+        ctx.clip();
+
+
+        // this is needed to prevent to see some lines between triangles
+        if (applyCorrection) {
+            coeff = 0.03;
+            x0 = ((1+coeff) * x0 - xc * coeff), y0 = ((1+coeff) * y0 - yc * coeff);
+            x1 = ((1+coeff) * x1 - xc * coeff), y1 = ((1+coeff) * y1 - yc * coeff);
+            x2 = ((1+coeff) * x2 - xc * coeff), y2 = ((1+coeff) * y2 - yc * coeff);
+        }
+
+        // ---- transform texture ----
+        var d_inv = 1/ (u0 * (v2 - v1) - u1 * v2 + u2 * v1 + (u1 - u2) * v0);
+        ctx.transform(
+            -(v0 * (x2 - x1) -  v1 * x2  + v2 *  x1 + (v1 - v2) * x0) * d_inv, // m11
+             (v1 *  y2 + v0  * (y1 - y2) - v2 *  y1 + (v2 - v1) * y0) * d_inv, // m12
+             (u0 * (x2 - x1) -  u1 * x2  + u2 *  x1 + (u1 - u2) * x0) * d_inv, // m21
+            -(u1 *  y2 + u0  * (y1 - y2) - u2 *  y1 + (u2 - u1) * y0) * d_inv, // m22
+             (u0 * (v2 * x1  -  v1 * x2) + v0 * (u1 *  x2 - u2  * x1) + (u2 * v1 - u1 * v2) * x0) * d_inv, // dx
+             (u0 * (v2 * y1  -  v1 * y2) + v0 * (u1 *  y2 - u2  * y1) + (u2 * v1 - u1 * v2) * y0) * d_inv  // dy
+        );
+        ctx.drawImage(img, 0, 0);
+        //ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);
+
+    //    ctx.globalAlpha = 1.0;
+
+        ctx.restore();
+    }
+
+    /*
+    function drawTexturedTriangle4Points(ctx, img, x0, y0, x1, y1, x2, y2,
+            u0, v0, u1, v1, u2, v2) {
+
+    	var x3 = x1+x2-x0;
+    	var y3 = y1+y2-y0;
+    // ---- centroid ----
+    var xc = (x0 + x1 + x2 + x3) / 4;
+    var yc = (y0 + y1 + y2 + y3) / 4;
+    ctx.save();
+    ctx.beginPath();
+    // ---- scale triagle by 1.05 to remove anti-aliasing and draw ----
+    ctx.moveTo((1.05 * x0 - xc * 0.05), (1.05 * y0 - yc * 0.05));
+    ctx.lineTo((1.05 * x1 - xc * 0.05), (1.05 * y1 - yc * 0.05));
+    ctx.lineTo((1.05 * x3 - xc * 0.05), (1.05 * y3 - yc * 0.05));
+    ctx.lineTo((1.05 * x2 - xc * 0.05), (1.05 * y2 - yc * 0.05));
+    ctx.closePath();
+    ctx.clip();
+    // ---- transform texture ----
+    var d_inv = 1/ (u0 * (v2 - v1) - u1 * v2 + u2 * v1 + (u1 - u2) * v0);
+    ctx.transform(
+    -(v0 * (x2 - x1) -  v1 * x2  + v2 *  x1 + (v1 - v2) * x0) * d_inv, // m11
+    (v1 *  y2 + v0  * (y1 - y2) - v2 *  y1 + (v2 - v1) * y0) * d_inv, // m12
+    (u0 * (x2 - x1) -  u1 * x2  + u2 *  x1 + (u1 - u2) * x0) * d_inv, // m21
+    -(u1 *  y2 + u0  * (y1 - y2) - u2 *  y1 + (u2 - u1) * y0) * d_inv, // m22
+    (u0 * (v2 * x1  -  v1 * x2) + v0 * (u1 *  x2 - u2  * x1) + (u2 * v1 - u1 * v2) * x0) * d_inv, // dx
+    (u0 * (v2 * y1  -  v1 * y2) + v0 * (u1 *  y2 - u2  * y1) + (u2 * v1 - u1 * v2) * y0) * d_inv  // dy
+    );
+    //ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height); // faster ??
+    ctx.drawImage(img, 0, 0); // slower ??
+
+    ctx.restore();
+    }
+    */
+
+
+    // @api
+    HpxImageSurvey.prototype.setAlpha = function(alpha) {
+        alpha = +alpha; // coerce to number
+        this.alpha = Math.max(0, Math.min(alpha, 1));
+        this.view.requestRedraw();
+    };
+
+    // @api
+    HpxImageSurvey.prototype.getAlpha = function() {
+        return this.alpha;
+    }
+
+    return HpxImageSurvey;
+})();
+// Copyright 2015 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File HealpixGrid
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+HealpixGrid = (function() {
+	var HealpixGrid = function() {
+	};
+
+	HealpixGrid.prototype.redraw = function(ctx, cornersXYViewMap, fov, norder) {
+		// on dessine les lignes
+		ctx.lineWidth = 1;
+		ctx.strokeStyle = "rgb(150,150,220)";
+		ctx.beginPath();
+		var cornersXYView;
+		for (var k=0, len=cornersXYViewMap.length; k<len; k++) {
+			cornersXYView = cornersXYViewMap[k];
+			ipix = cornersXYView.ipix;
+
+			// draw pixel
+			ctx.moveTo(cornersXYView[0].vx, cornersXYView[0].vy);
+			ctx.lineTo(cornersXYView[1].vx, cornersXYView[1].vy);
+			ctx.lineTo(cornersXYView[2].vx, cornersXYView[2].vy);
+			//ctx.lineTo(cornersXYView[3].vx, cornersXYView[3].vy);
+
+
+            //ctx.strokeText(ipix, (cornersXYView[0].vx + cornersXYView[2].vx)/2, (cornersXYView[0].vy + cornersXYView[2].vy)/2);
+		}
+		ctx.stroke();
+
+		// on dessine les numéros de pixel HEALpix
+        ctx.strokeStyle="#FFDDDD";
+		ctx.beginPath();
+		for (var k=0, len=cornersXYViewMap.length; k<len; k++) {
+			cornersXYView = cornersXYViewMap[k];
+			ipix = cornersXYView.ipix;
+
+            ctx.strokeText(norder + '/' + ipix, (cornersXYView[0].vx + cornersXYView[2].vx)/2, (cornersXYView[0].vy + cornersXYView[2].vy)/2);
+		}
+		ctx.stroke();
+	};
+
+
+
+	return HealpixGrid;
+})();
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File Location.js
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+Location = (function() {
+    // constructor
+    Location = function(locationDiv) {
+    		this.$div = $(locationDiv);
+    	};
+
+	Location.prototype.update = function(lon, lat, cooFrame, isViewCenterPosition) {
+        isViewCenterPosition = (isViewCenterPosition && isViewCenterPosition===true) || false;
+		var coo = new Coo(lon, lat, 7);
+		if (cooFrame==CooFrameEnum.J2000) {
+            this.$div.html(coo.format('s/'));
+        }
+		else if (cooFrame==CooFrameEnum.J2000d) {
+            this.$div.html(coo.format('d/'));
+        }
+        else {
+            this.$div.html(coo.format('d/'));
+        }
+
+        this.$div.toggleClass('aladin-reticleColor', isViewCenterPosition);
+	};
+
+	return Location;
+})();
+
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File View.js
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+View = (function() {
+
+    /** Constructor */
+    function View (aladin, location, fovDiv, cooFrame, zoom) {
+            this.aladin = aladin;
+            this.options = aladin.options;
+            this.aladinDiv = this.aladin.aladinDiv;
+            this.popup = new Popup(this.aladinDiv, this);
+
+            this.createCanvases();
+            this.location = location;
+            this.fovDiv = fovDiv;
+            this.mustClearCatalog = true;
+            this.mustRedrawReticle = true;
+
+            this.mode = View.PAN;
+
+            this.minFOV = this.maxFOV = null; // by default, no restriction
+
+            this.healpixGrid = new HealpixGrid(this.imageCanvas);
+            if (cooFrame) {
+                this.cooFrame = cooFrame;
+            }
+            else {
+                this.cooFrame = CooFrameEnum.GAL;
+            }
+
+            var lon, lat;
+            lon = lat = 0;
+
+            this.projectionMethod = ProjectionEnum.SIN;
+            this.projection = new Projection(lon, lat);
+            this.projection.setProjection(this.projectionMethod);
+            this.zoomLevel = 0;
+            this.zoomFactor = this.computeZoomFactor(this.zoomLevel);
+
+            this.viewCenter = {lon: lon, lat: lat}; // position of center of view
+
+            if (zoom) {
+                this.setZoom(zoom);
+            }
+
+            // current reference image survey displayed
+            this.imageSurvey = null;
+            // current catalogs displayed
+            this.catalogs = [];
+            // a dedicated catalog for the popup
+            var c = document.createElement('canvas');
+            c.width = c.height = 24;
+            var ctx= c.getContext('2d');
+            ctx.lineWidth = 6.0;
+            ctx.beginPath();
+            ctx.strokeStyle = '#eee';
+            ctx.arc(12, 12, 8, 0, 2*Math.PI, true);
+            ctx.stroke();
+            ctx.lineWidth = 3.0;
+            ctx.beginPath();
+            ctx.strokeStyle = '#c38';
+            ctx.arc(12, 12, 8, 0, 2*Math.PI, true);
+            ctx.stroke();
+            this.catalogForPopup = A.catalog({shape: c, sourceSize: 24});
+            //this.catalogForPopup = A.catalog({sourceSize: 18, shape: 'circle', color: '#c38'});
+            this.catalogForPopup.hide();
+            this.catalogForPopup.setView(this);
+            // overlays (footprints for instance)
+            this.overlays = [];
+            // MOCs
+            this.mocs = [];
+            // reference to all overlay layers (= catalogs + overlays + mocs)
+            this.allOverlayLayers = []
+
+
+
+            this.tileBuffer = new TileBuffer(); // tile buffer is shared across different image surveys
+            this.fixLayoutDimensions();
+
+
+            this.curNorder = 1;
+            this.realNorder = 1;
+            this.curOverlayNorder = 1;
+
+            // some variables for mouse handling
+            this.dragging = false;
+            this.dragx = null;
+            this.dragy = null;
+            this.needRedraw = true;
+
+            // zoom pinching
+            this.pinchZoomParameters = {
+                isPinching: false, // true if a pinch zoom is ongoing
+                initialFov: undefined,
+                initialDistance: undefined
+            };
+
+            this.downloader = new Downloader(this); // the downloader object is shared across all HpxImageSurveys
+            this.flagForceRedraw = false;
+
+            this.fadingLatestUpdate = null;
+
+            this.dateRequestRedraw = null;
+
+            this.showGrid = false; // coordinates grid
+
+            init(this);
+
+
+            // listen to window resize and reshape canvases
+            this.resizeTimer = null;
+            var self = this;
+            $(window).resize(function() {
+                clearTimeout(self.resizeTimer);
+                self.resizeTimer = setTimeout(function() {self.fixLayoutDimensions(self)}, 100);
+            });
+
+
+            // in some contexts (Jupyter notebook for instance), the parent div changes little time after Aladin Lite creation
+            // this results in canvas dimension to be incorrect.
+            // The following line tries to fix this issue
+            setTimeout(function() {
+                var computedWidth = $(self.aladinDiv).width();
+                var computedHeight = $(self.aladinDiv).height();
+
+                if (self.width!==computedWidth || self.height===computedHeight) {
+                    self.fixLayoutDimensions();
+                    self.setZoomLevel(self.zoomLevel); // needed to force recomputation of displayed FoV
+                }
+           }, 1000);
+        };
+
+    // different available modes
+    View.PAN = 0;
+    View.SELECT = 1;
+    View.TOOL_SIMBAD_POINTER = 2;
+
+
+    // TODO: should be put as an option at layer level
+    View.DRAW_SOURCES_WHILE_DRAGGING = true;
+    View.DRAW_MOCS_WHILE_DRAGGING = true;
+
+    View.CALLBACKS_THROTTLE_TIME_MS = 100; // minimum time between two consecutive callback calls
+
+
+    // (re)create needed canvases
+    View.prototype.createCanvases = function() {
+        var a = $(this.aladinDiv);
+        a.find('.aladin-imageCanvas').remove();
+        a.find('.aladin-catalogCanvas').remove();
+        a.find('.aladin-reticleCanvas').remove();
+
+        // canvas to draw the images
+        this.imageCanvas = $("<canvas class='aladin-imageCanvas'></canvas>").appendTo(this.aladinDiv)[0];
+        // canvas to draw the catalogs
+        this.catalogCanvas = $("<canvas class='aladin-catalogCanvas'></canvas>").appendTo(this.aladinDiv)[0];
+        // canvas to draw the reticle
+        this.reticleCanvas = $("<canvas class='aladin-reticleCanvas'></canvas>").appendTo(this.aladinDiv)[0];
+    };
+
+
+    // called at startup and when window is resized
+    View.prototype.fixLayoutDimensions = function() {
+        Utils.cssScale = undefined;
+
+        var computedWidth = $(this.aladinDiv).width();
+        var computedHeight = $(this.aladinDiv).height();
+
+        this.width = Math.max(computedWidth, 1);
+        this.height = Math.max(computedHeight, 1); // this prevents many problems when div size is equal to 0
+
+
+        this.cx = this.width/2;
+        this.cy = this.height/2;
+
+        this.largestDim = Math.max(this.width, this.height);
+        this.smallestDim = Math.min(this.width, this.height);
+        this.ratio = this.largestDim/this.smallestDim;
+
+
+        this.mouseMoveIncrement = 160/this.largestDim;
+
+        // reinitialize 2D context
+        this.imageCtx = this.imageCanvas.getContext("2d");
+        this.catalogCtx = this.catalogCanvas.getContext("2d");
+        this.reticleCtx = this.reticleCanvas.getContext("2d");
+
+        this.imageCtx.canvas.width = this.width;
+        this.catalogCtx.canvas.width = this.width;
+        this.reticleCtx.canvas.width = this.width;
+
+
+        this.imageCtx.canvas.height = this.height;
+        this.catalogCtx.canvas.height = this.height;
+        this.reticleCtx.canvas.height = this.height;
+
+        pixelateCanvasContext(this.imageCtx, this.aladin.options.pixelateCanvas);
+
+        // change logo
+        if (!this.logoDiv) {
+            this.logoDiv = $(this.aladinDiv).find('.aladin-logo')[0];
+        }
+        if (this.width>800) {
+            $(this.logoDiv).removeClass('aladin-logo-small');
+            $(this.logoDiv).addClass('aladin-logo-large');
+            $(this.logoDiv).css('width', '90px');
+        }
+        else {
+            $(this.logoDiv).addClass('aladin-logo-small');
+            $(this.logoDiv).removeClass('aladin-logo-large');
+            $(this.logoDiv).css('width', '32px');
+        }
+
+
+        this.computeNorder();
+        this.requestRedraw();
+    };
+
+    var pixelateCanvasContext = function(ctx, pixelateFlag) {
+        var enableSmoothing = ! pixelateFlag;
+        ctx.imageSmoothingEnabled = enableSmoothing;
+        ctx.webkitImageSmoothingEnabled = enableSmoothing;
+        ctx.mozImageSmoothingEnabled = enableSmoothing;
+        ctx.msImageSmoothingEnabled = enableSmoothing;
+        ctx.oImageSmoothingEnabled = enableSmoothing;
+    }
+
+
+    View.prototype.setMode = function(mode) {
+        this.mode = mode;
+        if (this.mode==View.SELECT) {
+            this.setCursor('crosshair');
+        }
+        else if (this.mode==View.TOOL_SIMBAD_POINTER) {
+            this.popup.hide();
+            this.reticleCanvas.style.cursor = '';
+            $(this.reticleCanvas).addClass('aladin-sp-cursor');
+        }
+        else {
+            this.setCursor('default');
+        }
+    };
+
+    View.prototype.setCursor = function(cursor) {
+        if (this.reticleCanvas.style.cursor==cursor) {
+            return;
+        }
+        if (this.mode==View.TOOL_SIMBAD_POINTER) {
+            return;
+        }
+        this.reticleCanvas.style.cursor = cursor;
+    };
+
+
+
+    /**
+     * return dataURL string corresponding to the current view
+     */
+    View.prototype.getCanvasDataURL = function(imgType, width, height) {
+        imgType = imgType || "image/png";
+        var c = document.createElement('canvas');
+        width = width || this.width;
+        height = height || this.height;
+        c.width = width;
+        c.height = height;
+        var ctx = c.getContext('2d');
+        ctx.drawImage(this.imageCanvas, 0, 0, c.width, c.height);
+        ctx.drawImage(this.catalogCanvas, 0, 0, c.width, c.height);
+        ctx.drawImage(this.reticleCanvas, 0, 0, c.width, c.height);
+
+        return c.toDataURL(imgType);
+        //return c.toDataURL("image/jpeg", 0.01); // setting quality only works for JPEG (?)
+    };
+
+
+    /**
+     * Compute the FoV in degrees of the view and update mouseMoveIncrement
+     *
+     * @param view
+     * @returns FoV (array of 2 elements : width and height) in degrees
+     */
+    computeFov = function(view) {
+        var fov = doComputeFov(view, view.zoomFactor);
+
+
+        view.mouseMoveIncrement = fov/view.imageCanvas.width;
+
+        return fov;
+    };
+
+    doComputeFov = function(view, zoomFactor) {
+        // if zoom factor < 1, we view 180°
+        var fov;
+        if (view.zoomFactor<1) {
+            fov = 180;
+        }
+        else {
+            // TODO : fov sur les 2 dimensions !!
+            // to compute FoV, we first retrieve 2 points at coordinates (0, view.cy) and (width-1, view.cy)
+            var xy1 = AladinUtils.viewToXy(0, view.cy, view.width, view.height, view.largestDim, zoomFactor);
+            var lonlat1 = view.projection.unproject(xy1.x, xy1.y);
+
+            var xy2 = AladinUtils.viewToXy(view.imageCanvas.width-1, view.cy, view.width, view.height, view.largestDim, zoomFactor);
+            var lonlat2 = view.projection.unproject(xy2.x, xy2.y);
+
+
+            fov = new Coo(lonlat1.ra, lonlat1.dec).distance(new Coo(lonlat2.ra, lonlat2.dec));
+        }
+
+        return fov;
+    };
+
+    updateFovDiv = function(view) {
+        if (isNaN(view.fov)) {
+            view.fovDiv.html("FoV:");
+            return;
+        }
+        // update FoV value
+        var fovStr;
+        if (view.fov>1) {
+            fovStr = Math.round(view.fov*100)/100 + "°";
+        }
+        else if (view.fov*60>1) {
+            fovStr = Math.round(view.fov*60*100)/100 + "'";
+        }
+        else {
+            fovStr = Math.round(view.fov*3600*100)/100 + '"';
+        }
+        view.fovDiv.html("FoV: " + fovStr);
+    };
+
+
+    createListeners = function(view) {
+        var hasTouchEvents = false;
+        if ('ontouchstart' in window) {
+            hasTouchEvents = true;
+        }
+
+        // various listeners
+        onDblClick = function(e) {
+            var xymouse = view.imageCanvas.relMouseCoords(e);
+            var xy = AladinUtils.viewToXy(xymouse.x, xymouse.y, view.width, view.height, view.largestDim, view.zoomFactor);
+            try {
+                var lonlat = view.projection.unproject(xy.x, xy.y);
+            }
+            catch(err) {
+                return;
+            }
+            radec = [];
+            // convert to J2000 if needed
+            if (view.cooFrame.system==CooFrameEnum.SYSTEMS.GAL) {
+                radec = CooConversion.GalacticToJ2000([lonlat.ra, lonlat.dec]);
+            }
+            else {
+                radec = [lonlat.ra, lonlat.dec];
+            }
+
+            view.pointTo(radec[0], radec[1]);
+        };
+        if (! hasTouchEvents) {
+            $(view.reticleCanvas).dblclick(onDblClick);
+        }
+
+
+        $(view.reticleCanvas).bind("mousedown touchstart", function(e) {
+            // zoom pinching
+            if (e.type==='touchstart' && e.originalEvent && e.originalEvent.targetTouches && e.originalEvent.targetTouches.length==2) {
+                view.dragging = false;
+
+                view.pinchZoomParameters.isPinching = true;
+                var fov = view.aladin.getFov();
+                view.pinchZoomParameters.initialFov = Math.max(fov[0], fov[1]);
+                view.pinchZoomParameters.initialDistance = Math.sqrt(Math.pow(e.originalEvent.targetTouches[0].clientX - e.originalEvent.targetTouches[1].clientX, 2) + Math.pow(e.originalEvent.targetTouches[0].clientY - e.originalEvent.targetTouches[1].clientY, 2));
+
+                return;
+            }
+
+            var xymouse = view.imageCanvas.relMouseCoords(e);
+            if (e.originalEvent && e.originalEvent.targetTouches) {
+                view.dragx = e.originalEvent.targetTouches[0].clientX;
+                view.dragy = e.originalEvent.targetTouches[0].clientY;
+            }
+            else {
+                /*
+                view.dragx = e.clientX;
+                view.dragy = e.clientY;
+                */
+                view.dragx = xymouse.x;
+                view.dragy = xymouse.y;
+            }
+
+
+            view.dragging = true;
+            if (view.mode==View.PAN) {
+                view.setCursor('move');
+            }
+            else if (view.mode==View.SELECT) {
+                view.selectStartCoo = {x: view.dragx, y: view.dragy};
+            }
+            return false; // to disable text selection
+        });
+
+        //$(view.reticleCanvas).bind("mouseup mouseout touchend", function(e) {
+        $(view.reticleCanvas).bind("click mouseout touchend", function(e) { // reacting on 'click' rather on 'mouseup' is more reliable when panning the view
+            if (e.type==='touchend' && view.pinchZoomParameters.isPinching) {
+                view.pinchZoomParameters.isPinching = false;
+                view.pinchZoomParameters.initialFov = view.pinchZoomParameters.initialDistance = undefined;
+
+                return;
+            }
+
+
+            var wasDragging = view.realDragging === true;
+            var selectionHasEnded = view.mode===View.SELECT && view.dragging;
+
+            if (view.dragging) { // if we were dragging, reset to default cursor
+                view.setCursor('default');
+                view.dragging = false;
+
+                if (wasDragging) {
+                    view.realDragging = false;
+
+                    // call positionChanged one last time after dragging, with dragging: false
+                    var posChangedFn = view.aladin.callbacksByEventName['positionChanged'];
+                    if (typeof posChangedFn === 'function') {
+                        var pos = view.aladin.pix2world(view.width/2, view.height/2);
+                        if (pos !== undefined) {
+                            posChangedFn({ra: pos[0], dec: pos[1], dragging: false});
+                        }
+                    }
+                }
+            } // end of "if (view.dragging) ... "
+
+            if (selectionHasEnded) {
+                view.aladin.fire('selectend',
+                                 view.getObjectsInBBox(view.selectStartCoo.x, view.selectStartCoo.y,
+                                                       view.dragx-view.selectStartCoo.x, view.dragy-view.selectStartCoo.y));
+
+                view.mustRedrawReticle = true; // pour effacer selection bounding box
+                view.requestRedraw();
+
+                return;
+            }
+
+
+
+            view.mustClearCatalog = true;
+            view.mustRedrawReticle = true; // pour effacer selection bounding box
+            view.dragx = view.dragy = null;
+
+
+
+            if (e.type==="mouseout" || e.type==="touchend") {
+                view.requestRedraw(true);
+                updateLocation(view, view.width/2, view.height/2, true);
+
+
+                if (e.type==="mouseout") {
+                    if (view.mode===View.TOOL_SIMBAD_POINTER) {
+                        view.setMode(View.PAN);
+                    }
+
+                    return;
+                }
+            }
+
+            var xymouse = view.imageCanvas.relMouseCoords(e);
+
+            if (view.mode==View.TOOL_SIMBAD_POINTER) {
+                var radec = view.aladin.pix2world(xymouse.x, xymouse.y);
+
+                view.setMode(View.PAN);
+                view.setCursor('wait');
+
+                SimbadPointer.query(radec[0], radec[1], Math.min(1, 15 * view.fov / view.largestDim), view.aladin);
+
+                return; // when in TOOL_SIMBAD_POINTER mode, we do not call the listeners
+            }
+
+            // popup to show ?
+            var objs = view.closestObjects(xymouse.x, xymouse.y, 5);
+            if (! wasDragging && objs) {
+                var o = objs[0];
+
+                // footprint selection code adapted from Fabrizio Giordano dev. from Serco for ESA/ESDC
+                if (o instanceof Footprint || o instanceof Circle) {
+                    o.dispatchClickEvent();
+                }
+
+                // display marker
+                else if (o.marker) {
+                    // could be factorized in Source.actionClicked
+                    view.popup.setTitle(o.popupTitle);
+                    view.popup.setText(o.popupDesc);
+                    view.popup.setSource(o);
+                    view.popup.show();
+                }
+                // show measurements
+                else {
+                    if (view.lastClickedObject) {
+                        view.lastClickedObject.actionOtherObjectClicked && view.lastClickedObject.actionOtherObjectClicked();
+                    }
+                    o.actionClicked();
+                }
+                view.lastClickedObject = o;
+                var objClickedFunction = view.aladin.callbacksByEventName['objectClicked'];
+                (typeof objClickedFunction === 'function') && objClickedFunction(o);
+            }
+            else {
+                if (view.lastClickedObject && ! wasDragging) {
+                    view.aladin.measurementTable.hide();
+                    view.popup.hide();
+
+                    if (view.lastClickedObject instanceof Footprint) {
+                        //view.lastClickedObject.deselect();
+                    }
+                    else {
+                        view.lastClickedObject.actionOtherObjectClicked();
+                    }
+
+                    view.lastClickedObject = null;
+                    var objClickedFunction = view.aladin.callbacksByEventName['objectClicked'];
+                    (typeof objClickedFunction === 'function') && objClickedFunction(null);
+                }
+            }
+
+            // call listener of 'click' event
+            var onClickFunction = view.aladin.callbacksByEventName['click'];
+            if (typeof onClickFunction === 'function') {
+                var pos = view.aladin.pix2world(xymouse.x, xymouse.y);
+                if (pos !== undefined) {
+                    onClickFunction({ra: pos[0], dec: pos[1], x: xymouse.x, y: xymouse.y, isDragging: wasDragging});
+                }
+            }
+
+
+            // TODO : remplacer par mecanisme de listeners
+            // on avertit les catalogues progressifs
+            view.refreshProgressiveCats();
+
+            view.requestRedraw(true);
+        });
+        var lastHoveredObject; // save last object hovered by mouse
+        var lastMouseMovePos = null;
+        $(view.reticleCanvas).bind("mousemove touchmove", function(e) {
+            e.preventDefault();
+
+            if (e.type==='touchmove' && view.pinchZoomParameters.isPinching && e.originalEvent && e.originalEvent.touches && e.originalEvent.touches.length==2) {
+                var dist = Math.sqrt(Math.pow(e.originalEvent.touches[0].clientX - e.originalEvent.touches[1].clientX, 2) + Math.pow(e.originalEvent.touches[0].clientY - e.originalEvent.touches[1].clientY, 2));
+                view.setZoom(view.pinchZoomParameters.initialFov * view.pinchZoomParameters.initialDistance / dist);
+
+                return;
+            }
+
+            var xymouse = view.imageCanvas.relMouseCoords(e);
+            if (!view.dragging || hasTouchEvents) {
+                // update location box
+                updateLocation(view, xymouse.x, xymouse.y);
+                // call listener of 'mouseMove' event
+                var onMouseMoveFunction = view.aladin.callbacksByEventName['mouseMove'];
+                if (typeof onMouseMoveFunction === 'function') {
+                    var pos = view.aladin.pix2world(xymouse.x, xymouse.y);
+                    if (pos !== undefined) {
+                        onMouseMoveFunction({ra: pos[0], dec: pos[1], x: xymouse.x, y: xymouse.y});
+                    }
+                    // send null ra and dec when we go out of the "sky"
+                    else if (lastMouseMovePos != null) {
+                        onMouseMoveFunction({ra: null, dec: null, x: xymouse.x, y: xymouse.y});
+                    }
+                    lastMouseMovePos = pos;
+                }
+
+
+                if (!view.dragging && ! view.mode==View.SELECT) {
+                    // objects under the mouse ?
+                    var closest = view.closestObjects(xymouse.x, xymouse.y, 5);
+                    if (closest) {
+                        view.setCursor('pointer');
+                        var objHoveredFunction = view.aladin.callbacksByEventName['objectHovered'];
+                        if (typeof objHoveredFunction === 'function' && closest[0]!=lastHoveredObject) {
+                            var ret = objHoveredFunction(closest[0]);
+                        }
+                        lastHoveredObject = closest[0];
+
+                    }
+                    else {
+                        view.setCursor('default');
+                        var objHoveredFunction = view.aladin.callbacksByEventName['objectHovered'];
+                        if (typeof objHoveredFunction === 'function' && lastHoveredObject) {
+                            lastHoveredObject = null;
+                            // call callback function to notify we left the hovered object
+                            var ret = objHoveredFunction(null);
+                        }
+                    }
+                }
+                if (!hasTouchEvents) {
+                    return;
+                }
+            }
+
+            if (! view.dragging) {
+                return;
+            }
+
+            var xoffset, yoffset;
+            var pos1, pos2;
+
+            if (e.originalEvent && e.originalEvent.targetTouches) {
+                // ???
+                xoffset = e.originalEvent.targetTouches[0].clientX-view.dragx;
+                yoffset = e.originalEvent.targetTouches[0].clientY-view.dragy;
+                var xy1 = AladinUtils.viewToXy(e.originalEvent.targetTouches[0].clientX, e.originalEvent.targetTouches[0].clientY, view.width, view.height, view.largestDim, view.zoomFactor);
+                var xy2 = AladinUtils.viewToXy(view.dragx, view.dragy, view.width, view.height, view.largestDim, view.zoomFactor);
+
+                pos1 = view.projection.unproject(xy1.x, xy1.y);
+                pos2 = view.projection.unproject(xy2.x, xy2.y);
+            }
+            else {
+                /*
+                xoffset = e.clientX-view.dragx;
+                yoffset = e.clientY-view.dragy;
+                */
+                xoffset = xymouse.x-view.dragx;
+                yoffset = xymouse.y-view.dragy;
+
+                var xy1 = AladinUtils.viewToXy(xymouse.x, xymouse.y, view.width, view.height, view.largestDim, view.zoomFactor);
+                var xy2 = AladinUtils.viewToXy(view.dragx, view.dragy, view.width, view.height, view.largestDim, view.zoomFactor);
+
+
+                pos1 = view.projection.unproject(xy1.x, xy1.y);
+                pos2 = view.projection.unproject(xy2.x, xy2.y);
+
+            }
+
+            // TODO : faut il faire ce test ??
+//            var distSquared = xoffset*xoffset+yoffset*yoffset;
+//            if (distSquared<3) {
+//                return;
+//            }
+            if (e.originalEvent && e.originalEvent.targetTouches) {
+                view.dragx = e.originalEvent.targetTouches[0].clientX;
+                view.dragy = e.originalEvent.targetTouches[0].clientY;
+            }
+            else {
+                view.dragx = xymouse.x;
+                view.dragy = xymouse.y;
+                /*
+                view.dragx = e.clientX;
+                view.dragy = e.clientY;
+                */
+            }
+
+            if (view.mode==View.SELECT) {
+                  view.requestRedraw();
+                  return;
+            }
+
+            //view.viewCenter.lon += xoffset*view.mouseMoveIncrement/Math.cos(view.viewCenter.lat*Math.PI/180.0);
+            /*
+            view.viewCenter.lon += xoffset*view.mouseMoveIncrement;
+            view.viewCenter.lat += yoffset*view.mouseMoveIncrement;
+            */
+            view.viewCenter.lon += pos2.ra -  pos1.ra;
+            view.viewCenter.lat += pos2.dec - pos1.dec;
+
+
+
+            // can not go beyond poles
+            if (view.viewCenter.lat>90) {
+                view.viewCenter.lat = 90;
+            }
+            else if (view.viewCenter.lat < -90) {
+                view.viewCenter.lat = -90;
+            }
+
+            // limit lon to [0, 360]
+            if (view.viewCenter.lon < 0) {
+                view.viewCenter.lon = 360 + view.viewCenter.lon;
+            }
+            else if (view.viewCenter.lon > 360) {
+                view.viewCenter.lon = view.viewCenter.lon % 360;
+            }
+            view.realDragging = true;
+            view.requestRedraw();
+        }); //// endof mousemove ////
+
+        // disable text selection on IE
+        $(view.aladinDiv).onselectstart = function () { return false; }
+
+        $(view.reticleCanvas).on('mousewheel', function(event) {
+            event.preventDefault();
+            event.stopPropagation();
+            var level = view.zoomLevel;
+
+             var delta = event.deltaY;
+            // this seems to happen in context of Jupyter notebook --> we have to invert the direction of scroll
+            // hope this won't trigger some side effects ...
+            if (event.hasOwnProperty('originalEvent')) {
+                delta = -event.originalEvent.deltaY;
+            }
+            if (delta>0) {
+                level += 1;
+            }
+            else {
+                level -= 1;
+            }
+            view.setZoomLevel(level);
+
+            return false;
+        });
+
+    };
+
+    var init = function(view) {
+        var stats = new Stats();
+        stats.domElement.style.top = '50px';
+        if ($('#aladin-statsDiv').length>0) {
+            $('#aladin-statsDiv')[0].appendChild( stats.domElement );
+        }
+
+        view.stats = stats;
+
+        createListeners(view);
+
+        view.executeCallbacksThrottled = Utils.throttle(
+            function() {
+                var pos = view.aladin.pix2world(view.width/2, view.height/2);
+                var fov = view.fov;
+                if (pos===undefined || fov===undefined) {
+                    return;
+                }
+
+                var ra = pos[0];
+                var dec = pos[1];
+                // trigger callback only if position has changed !
+                if (ra!==this.ra || dec!==this.dec) {
+                    var posChangedFn = view.aladin.callbacksByEventName['positionChanged'];
+                    (typeof posChangedFn === 'function') && posChangedFn({ra: ra, dec: dec, dragging: true});
+
+                    // finally, save ra and dec value
+                    this.ra = ra;
+                    this.dec = dec;
+                }
+
+                // trigger callback only if FoV (zoom) has changed !
+                if (fov!==this.old_fov) {
+                    var fovChangedFn = view.aladin.callbacksByEventName['zoomChanged'];
+                    (typeof fovChangedFn === 'function') && fovChangedFn(fov);
+
+                    // finally, save fov value
+                    this.old_fov = fov;
+                }
+
+            },
+            View.CALLBACKS_THROTTLE_TIME_MS);
+
+
+        view.displayHpxGrid = false;
+        view.displaySurvey = true;
+        view.displayCatalog = false;
+        view.displayReticle = true;
+
+        // initial draw
+        view.fov = computeFov(view);
+        updateFovDiv(view);
+
+        view.redraw();
+    };
+
+    function updateLocation(view, x, y, isViewCenterPosition) {
+        if (!view.projection) {
+            return;
+        }
+        var xy = AladinUtils.viewToXy(x, y, view.width, view.height, view.largestDim, view.zoomFactor);
+        var lonlat;
+        try {
+            lonlat = view.projection.unproject(xy.x, xy.y);
+        }
+        catch(err) {
+        }
+        if (lonlat) {
+            view.location.update(lonlat.ra, lonlat.dec, view.cooFrame, isViewCenterPosition);
+        }
+    }
+
+    View.prototype.requestRedrawAtDate = function(date) {
+        this.dateRequestDraw = date;
+    };
+
+    /**
+     * Return the color of the lowest intensity pixel
+     * in teh current color map of the current background image HiPS
+     */
+    View.prototype.getBackgroundColor = function() {
+        var white = 'rgb(255, 255, 255)';
+        var black = 'rgb(0, 0, 0)';
+
+        if (! this.imageSurvey) {
+            return black;
+        }
+
+        var cm = this.imageSurvey.getColorMap();
+        if (!cm) {
+            return black;
+        }
+        if (cm.mapName == 'native' || cm.mapName == 'grayscale') {
+            return cm.reversed ? white : black;
+        }
+
+        var idx = cm.reversed ? 255 : 0;
+        var r = ColorMap.MAPS[cm.mapName].r[idx];
+        var g = ColorMap.MAPS[cm.mapName].g[idx];
+        var b = ColorMap.MAPS[cm.mapName].b[idx];
+
+        return 'rgb(' + r + ',' + g + ',' + b + ')';
+    };
+
+    View.prototype.getViewParams = function() {
+        var resolution = this.width > this.height ? this.fov / this.width : this.fov / this.height;
+        return {
+            fov: [this.width * resolution, this.height * resolution],
+            width: this.width,
+            height: this.height
+        };
+    };
+
+
+
+    /**
+     * redraw the whole view
+     */
+    View.prototype.redraw = function() {
+        var saveNeedRedraw = this.needRedraw;
+        requestAnimFrame(this.redraw.bind(this));
+
+        var now = new Date().getTime();
+
+        if (this.dateRequestDraw && now>this.dateRequestDraw) {
+            this.dateRequestDraw = null;
+        }
+        else if (! this.needRedraw) {
+            if ( ! this.flagForceRedraw) {
+                return;
+            }
+            else {
+                this.flagForceRedraw = false;
+            }
+        }
+        this.stats.update();
+
+
+        var imageCtx = this.imageCtx;
+        //////// 1. Draw images ////////
+        if (imageCtx.start2D) {
+            imageCtx.start2D();
+        }
+        //// clear canvas ////
+        // TODO : do not need to clear if fov small enough ?
+        imageCtx.clearRect(0, 0, this.imageCanvas.width, this.imageCanvas.height);
+        ////////////////////////
+
+        var bkgdColor = this.getBackgroundColor();
+        // fill with background of the same color than the first color map value (lowest intensity)
+        if (this.projectionMethod==ProjectionEnum.SIN) {
+            if (this.fov>=60) {
+                imageCtx.fillStyle = bkgdColor;
+                imageCtx.beginPath();
+                var maxCxCy = this.cx>this.cy ? this.cx : this.cy;
+                imageCtx.arc(this.cx, this.cy, maxCxCy * this.zoomFactor, 0, 2*Math.PI, true);
+                imageCtx.fill();
+            }
+            // pour eviter les losanges blancs qui apparaissent quand les tuiles sont en attente de chargement
+            else {
+                imageCtx.fillStyle = bkgdColor;
+                imageCtx.fillRect(0, 0, this.imageCanvas.width, this.imageCanvas.height);
+            }
+        }
+        else if (this.projectionMethod==ProjectionEnum.AITOFF) {
+            if (imageCtx.ellipse) {
+                imageCtx.fillStyle = bkgdColor;
+                imageCtx.beginPath();
+                imageCtx.ellipse(this.cx, this.cy, 2.828*this.cx*this.zoomFactor, this.cx*this.zoomFactor*1.414, 0, 0, 2*Math.PI);
+                imageCtx.fill();
+            }
+        }
+        if (imageCtx.finish2D) {
+            imageCtx.finish2D();
+        }
+
+
+        this.projection.setCenter(this.viewCenter.lon, this.viewCenter.lat);
+        // do we have to redo that every time? Probably not
+        this.projection.setProjection(this.projectionMethod);
+
+
+        // ************* Draw allsky tiles (low resolution) *****************
+
+        var cornersXYViewMapHighres = null;
+        // Pour traitement des DEFORMATIONS --> TEMPORAIRE, draw deviendra la methode utilisee systematiquement
+        if (this.imageSurvey && this.imageSurvey.isReady && this.displaySurvey) {
+                if (this.aladin.reduceDeformations==null) {
+                    this.imageSurvey.draw(imageCtx, this, !this.dragging, this.curNorder);
+                }
+
+                else {
+                    this.imageSurvey.draw(imageCtx, this, this.aladin.reduceDeformations, this.curNorder);
+                }
+        }
+        /*
+        else {
+            var cornersXYViewMapAllsky = this.getVisibleCells(3);
+            var cornersXYViewMapHighres = null;
+            if (this.curNorder>=3) {
+                if (this.curNorder==3) {
+                    cornersXYViewMapHighres = cornersXYViewMapAllsky;
+                }
+                else {
+                    cornersXYViewMapHighres = this.getVisibleCells(this.curNorder);
+                }
+            }
+
+            // redraw image survey
+            if (this.imageSurvey && this.imageSurvey.isReady && this.displaySurvey) {
+                // TODO : a t on besoin de dessiner le allsky si norder>=3 ?
+                // TODO refactoring : should be a method of HpxImageSurvey
+                this.imageSurvey.redrawAllsky(imageCtx, cornersXYViewMapAllsky, this.fov, this.curNorder);
+                if (this.curNorder>=3) {
+                    this.imageSurvey.redrawHighres(imageCtx, cornersXYViewMapHighres, this.curNorder);
+                }
+            }
+        }
+        */
+
+
+        // redraw overlay image survey
+        // TODO : does not work if different frames
+        // TODO: use HpxImageSurvey.draw method !!
+        if (this.overlayImageSurvey && this.overlayImageSurvey.isReady) {
+            imageCtx.globalAlpha = this.overlayImageSurvey.getAlpha();
+
+            if (this.aladin.reduceDeformations==null) {
+                this.overlayImageSurvey.draw(imageCtx, this, !this.dragging, this.curOverlayNorder);
+            }
+
+            else {
+                this.overlayImageSurvey.draw(imageCtx, this, this.aladin.reduceDeformations, this.curOverlayNorder);
+            }
+            /*
+            if (this.fov>50) {
+                this.overlayImageSurvey.redrawAllsky(imageCtx, cornersXYViewMapAllsky, this.fov, this.curOverlayNorder);
+            }
+            if (this.curOverlayNorder>=3) {
+                var norderOverlay = Math.min(this.curOverlayNorder, this.overlayImageSurvey.maxOrder);
+                if ( cornersXYViewMapHighres==null || norderOverlay != this.curNorder ) {
+                    cornersXYViewMapHighres = this.getVisibleCells(norderOverlay);
+                }
+                this.overlayImageSurvey.redrawHighres(imageCtx, cornersXYViewMapHighres, norderOverlay);
+            }
+            */
+
+           imageCtx.globalAlpha = 1.0;
+
+        }
+
+
+        // redraw HEALPix grid
+        if( this.displayHpxGrid) {
+            var cornersXYViewMapAllsky = this.getVisibleCells(3);
+            var cornersXYViewMapHighres = null;
+            if (this.curNorder>=3) {
+                if (this.curNorder==3) {
+                    cornersXYViewMapHighres = cornersXYViewMapAllsky;
+                }
+                else {
+                    cornersXYViewMapHighres = this.getVisibleCells(this.curNorder);
+                }
+            }
+            if (cornersXYViewMapHighres && this.curNorder>3) {
+                this.healpixGrid.redraw(imageCtx, cornersXYViewMapHighres, this.fov, this.curNorder);
+            }
+            else {
+                this.healpixGrid.redraw(imageCtx, cornersXYViewMapAllsky, this.fov, 3);
+            }
+        }
+
+        // redraw coordinates grid
+        if (this.showGrid) {
+            if (this.cooGrid==null) {
+                this.cooGrid = new CooGrid();
+            }
+
+            this.cooGrid.redraw(imageCtx, this.projection, this.cooFrame, this.width, this.height, this.largestDim, this.zoomFactor, this.fov);
+        }
+
+
+
+
+        ////// 2. Draw catalogues////////
+        var catalogCtx = this.catalogCtx;
+
+        var catalogCanvasCleared = false;
+        if (this.mustClearCatalog) {
+            catalogCtx.clearRect(0, 0, this.width, this.height);
+            catalogCanvasCleared = true;
+            this.mustClearCatalog = false;
+        }
+        if (this.catalogs && this.catalogs.length>0 && this.displayCatalog && (! this.dragging  || View.DRAW_SOURCES_WHILE_DRAGGING)) {
+              // TODO : do not clear every time
+            //// clear canvas ////
+            if (! catalogCanvasCleared) {
+                catalogCtx.clearRect(0, 0, this.width, this.height);
+                catalogCanvasCleared = true;
+            }
+            for (var i=0; i<this.catalogs.length; i++) {
+                var cat = this.catalogs[i];
+                cat.draw(catalogCtx, this.projection, this.cooFrame, this.width, this.height, this.largestDim, this.zoomFactor);
+            }
+        }
+        // draw popup catalog
+        if (this.catalogForPopup.isShowing && this.catalogForPopup.sources.length>0) {
+            if (! catalogCanvasCleared) {
+                catalogCtx.clearRect(0, 0, this.width, this.height);
+                catalogCanvasCleared = true;
+            }
+            this.catalogForPopup.draw(catalogCtx, this.projection, this.cooFrame, this.width, this.height, this.largestDim, this.zoomFactor);
+        }
+
+        ////// 3. Draw overlays////////
+        var overlayCtx = this.catalogCtx;
+        if (this.overlays && this.overlays.length>0 && (! this.dragging  || View.DRAW_SOURCES_WHILE_DRAGGING)) {
+            if (! catalogCanvasCleared) {
+                catalogCtx.clearRect(0, 0, this.width, this.height);
+                catalogCanvasCleared = true;
+            }
+            for (var i=0; i<this.overlays.length; i++) {
+                this.overlays[i].draw(overlayCtx, this.projection, this.cooFrame, this.width, this.height, this.largestDim, this.zoomFactor);
+            }
+        }
+
+
+        // draw MOCs
+        var mocCtx = this.catalogCtx;
+        if (this.mocs && this.mocs.length>0 && (! this.dragging  || View.DRAW_MOCS_WHILE_DRAGGING)) {
+            if (! catalogCanvasCleared) {
+                catalogCtx.clearRect(0, 0, this.width, this.height);
+                catalogCanvasCleared = true;
+            }
+            for (var i=0; i<this.mocs.length; i++) {
+                this.mocs[i].draw(mocCtx, this.projection, this.cooFrame, this.width, this.height, this.largestDim, this.zoomFactor, this.fov);
+            }
+        }
+
+
+        if (this.mode==View.SELECT) {
+            mustRedrawReticle = true;
+        }
+        ////// 4. Draw reticle ///////
+        // TODO: reticle should be placed in a static DIV, no need to waste a canvas
+        var reticleCtx = this.reticleCtx;
+        if (this.mustRedrawReticle || this.mode==View.SELECT) {
+            reticleCtx.clearRect(0, 0, this.width, this.height);
+        }
+        if (this.displayReticle) {
+
+            if (! this.reticleCache) {
+                // build reticle image
+                var c = document.createElement('canvas');
+                var s = this.options.reticleSize;
+                c.width = s;
+                c.height = s;
+                var ctx = c.getContext('2d');
+                ctx.lineWidth = 2;
+                ctx.strokeStyle = this.options.reticleColor;
+                ctx.beginPath();
+                ctx.moveTo(s/2, s/2+(s/2-1));
+                ctx.lineTo(s/2, s/2+2);
+                ctx.moveTo(s/2, s/2-(s/2-1));
+                ctx.lineTo(s/2, s/2-2);
+
+                ctx.moveTo(s/2+(s/2-1), s/2);
+                ctx.lineTo(s/2+2,  s/2);
+                ctx.moveTo(s/2-(s/2-1), s/2);
+                ctx.lineTo(s/2-2,  s/2);
+
+                ctx.stroke();
+
+                this.reticleCache = c;
+            }
+
+            reticleCtx.drawImage(this.reticleCache, this.width/2 - this.reticleCache.width/2, this.height/2 - this.reticleCache.height/2);
+
+
+            this.mustRedrawReticle = false;
+        }
+
+        ////// 5. Draw all-sky ring /////
+        if (this.projectionMethod==ProjectionEnum.SIN && this.fov>=60 && this.aladin.options['showAllskyRing'] === true) {
+                    imageCtx.strokeStyle = this.aladin.options['allskyRingColor'];
+                    var ringWidth = this.aladin.options['allskyRingWidth'];
+                    imageCtx.lineWidth = ringWidth;
+                    imageCtx.beginPath();
+                    var maxCxCy = this.cx>this.cy ? this.cx : this.cy;
+                    imageCtx.arc(this.cx, this.cy, (maxCxCy-(ringWidth/2.0)+1) * this.zoomFactor, 0, 2*Math.PI, true);
+                    imageCtx.stroke();
+        }
+
+
+        // draw selection box
+        if (this.mode==View.SELECT && this.dragging) {
+            reticleCtx.fillStyle = "rgba(100, 240, 110, 0.25)";
+            var w = this.dragx - this.selectStartCoo.x;
+            var h =  this.dragy - this.selectStartCoo.y;
+
+            reticleCtx.fillRect(this.selectStartCoo.x, this.selectStartCoo.y, w, h);
+        }
+
+
+         // TODO : is this the right way?
+         if (saveNeedRedraw==this.needRedraw) {
+             this.needRedraw = false;
+         }
+
+
+        // objects lookup
+        if (!this.dragging) {
+            this.updateObjectsLookup();
+        }
+
+        // execute 'positionChanged' and 'zoomChanged' callbacks
+        this.executeCallbacksThrottled();
+
+    };
+
+    View.prototype.forceRedraw = function() {
+        this.flagForceRedraw = true;
+    };
+
+    View.prototype.refreshProgressiveCats = function() {
+        if (! this.catalogs) {
+            return;
+        }
+        for (var i=0; i<this.catalogs.length; i++) {
+            if (this.catalogs[i].type=='progressivecat') {
+                this.catalogs[i].loadNeededTiles();
+            }
+        }
+    };
+
+    View.prototype.getVisiblePixList = function(norder, frameSurvey) {
+        var nside = Math.pow(2, norder);
+
+        var pixList;
+        var npix = HealpixIndex.nside2Npix(nside);
+        if (this.fov>80) {
+            pixList = [];
+            for (var ipix=0; ipix<npix; ipix++) {
+                pixList.push(ipix);
+            }
+        }
+        else {
+            var hpxIdx = new HealpixIndex(nside);
+            hpxIdx.init();
+            var spatialVector = new SpatialVector();
+            // if frame != frame image survey, we need to convert to survey frame system
+            var xy = AladinUtils.viewToXy(this.cx, this.cy, this.width, this.height, this.largestDim, this.zoomFactor);
+            var radec = this.projection.unproject(xy.x, xy.y);
+            var lonlat = [];
+            if (frameSurvey && frameSurvey.system != this.cooFrame.system) {
+                if (frameSurvey.system==CooFrameEnum.SYSTEMS.J2000) {
+                    lonlat = CooConversion.GalacticToJ2000([radec.ra, radec.dec]);
+                }
+                else if (frameSurvey.system==CooFrameEnum.SYSTEMS.GAL) {
+                    lonlat = CooConversion.J2000ToGalactic([radec.ra, radec.dec]);
+                }
+            }
+            else {
+                lonlat = [radec.ra, radec.dec];
+            }
+            if (this.imageSurvey && this.imageSurvey.longitudeReversed===true) {
+                spatialVector.set(lonlat[0], lonlat[1]);
+            }
+            else {
+                spatialVector.set(lonlat[0], lonlat[1]);
+            }
+            var radius = this.fov*0.5*this.ratio;
+            // we need to extend the radius
+            if (this.fov>60) {
+                radius *= 1.6;
+            }
+            else if (this.fov>12) {
+                radius *=1.45;
+            }
+            else {
+                radius *= 1.1;
+            }
+
+
+
+            pixList = hpxIdx.queryDisc(spatialVector, radius*Math.PI/180.0, true, true);
+            // add central pixel at index 0
+            var polar = Utils.radecToPolar(lonlat[0], lonlat[1]);
+            ipixCenter = hpxIdx.ang2pix_nest(polar.theta, polar.phi);
+            pixList.unshift(ipixCenter);
+
+        }
+
+        return pixList;
+    };
+
+    // TODO: optimize this method !!
+    View.prototype.getVisibleCells = function(norder, frameSurvey) {
+        if (! frameSurvey && this.imageSurvey) {
+            frameSurvey = this.imageSurvey.cooFrame;
+        }
+        var cells = []; // array to be returned
+        var cornersXY = [];
+        var spVec = new SpatialVector();
+        var nside = Math.pow(2, norder); // TODO : to be modified
+        var npix = HealpixIndex.nside2Npix(nside);
+        var ipixCenter = null;
+
+        // build list of pixels
+        // TODO: pixList can be obtained from getVisiblePixList
+        var pixList;
+        if (this.fov>80) {
+            pixList = [];
+            for (var ipix=0; ipix<npix; ipix++) {
+                pixList.push(ipix);
+            }
+        }
+        else {
+            var hpxIdx = new HealpixIndex(nside);
+            hpxIdx.init();
+            var spatialVector = new SpatialVector();
+            // if frame != frame image survey, we need to convert to survey frame system
+            var xy = AladinUtils.viewToXy(this.cx, this.cy, this.width, this.height, this.largestDim, this.zoomFactor);
+            var radec = this.projection.unproject(xy.x, xy.y);
+            var lonlat = [];
+            if (frameSurvey && frameSurvey.system != this.cooFrame.system) {
+                if (frameSurvey.system==CooFrameEnum.SYSTEMS.J2000) {
+                    lonlat = CooConversion.GalacticToJ2000([radec.ra, radec.dec]);
+                }
+                else if (frameSurvey.system==CooFrameEnum.SYSTEMS.GAL) {
+                    lonlat = CooConversion.J2000ToGalactic([radec.ra, radec.dec]);
+                }
+            }
+            else {
+                lonlat = [radec.ra, radec.dec];
+            }
+            if (this.imageSurvey && this.imageSurvey.longitudeReversed===true) {
+                spatialVector.set(lonlat[0], lonlat[1]);
+            }
+            else {
+                spatialVector.set(lonlat[0], lonlat[1]);
+            }
+            var radius = this.fov*0.5*this.ratio;
+            // we need to extend the radius
+            if (this.fov>60) {
+                radius *= 1.6;
+            }
+            else if (this.fov>12) {
+                radius *=1.45;
+            }
+            else {
+                radius *= 1.1;
+            }
+
+
+
+            pixList = hpxIdx.queryDisc(spatialVector, radius*Math.PI/180.0, true, true);
+            // add central pixel at index 0
+            var polar = Utils.radecToPolar(lonlat[0], lonlat[1]);
+            ipixCenter = hpxIdx.ang2pix_nest(polar.theta, polar.phi);
+            pixList.unshift(ipixCenter);
+        }
+
+
+        var ipix;
+        var lon, lat;
+        for (var ipixIdx=0, len=pixList.length; ipixIdx<len; ipixIdx++) {
+            ipix = pixList[ipixIdx];
+            if (ipix==ipixCenter && ipixIdx>0) {
+                continue;
+            }
+            var cornersXYView = [];
+            corners = HealpixCache.corners_nest(ipix, nside);
+
+            for (var k=0; k<4; k++) {
+                spVec.setXYZ(corners[k].x, corners[k].y, corners[k].z);
+
+                // need for frame transformation ?
+                if (frameSurvey && frameSurvey.system != this.cooFrame.system) {
+                    if (frameSurvey.system == CooFrameEnum.SYSTEMS.J2000) {
+                        var radec = CooConversion.J2000ToGalactic([spVec.ra(), spVec.dec()]);
+                        lon = radec[0];
+                        lat = radec[1];
+                    }
+                    else if (frameSurvey.system == CooFrameEnum.SYSTEMS.GAL) {
+                        var radec = CooConversion.GalacticToJ2000([spVec.ra(), spVec.dec()]);
+                        lon = radec[0];
+                        lat = radec[1];
+                    }
+                }
+                else {
+                    lon = spVec.ra();
+                    lat = spVec.dec();
+                }
+
+                cornersXY[k] = this.projection.project(lon, lat);
+            }
+
+
+            if (cornersXY[0] == null ||  cornersXY[1] == null  ||  cornersXY[2] == null ||  cornersXY[3] == null ) {
+                continue;
+            }
+
+
+
+            for (var k=0; k<4; k++) {
+                cornersXYView[k] = AladinUtils.xyToView(cornersXY[k].X, cornersXY[k].Y, this.width, this.height, this.largestDim, this.zoomFactor);
+            }
+
+            var indulge = 10;
+            // detect pixels outside view. Could be improved !
+            // we minimize here the number of cells returned
+            if( cornersXYView[0].vx<0 && cornersXYView[1].vx<0 && cornersXYView[2].vx<0 &&cornersXYView[3].vx<0) {
+                continue;
+            }
+            if( cornersXYView[0].vy<0 && cornersXYView[1].vy<0 && cornersXYView[2].vy<0 &&cornersXYView[3].vy<0) {
+                continue;
+            }
+            if( cornersXYView[0].vx>=this.width && cornersXYView[1].vx>=this.width && cornersXYView[2].vx>=this.width &&cornersXYView[3].vx>=this.width) {
+                continue;
+            }
+            if( cornersXYView[0].vy>=this.height && cornersXYView[1].vy>=this.height && cornersXYView[2].vy>=this.height &&cornersXYView[3].vy>=this.height) {
+                continue;
+            }
+
+
+            // check if pixel is visible
+//            if (this.fov<160) { // don't bother checking if fov is large enough
+//                if ( ! AladinUtils.isHpxPixVisible(cornersXYView, this.width, this.height) ) {
+//                    continue;
+//                }
+//            }
+            // check if we have a pixel at the edge of the view in AITOFF --> TO BE MODIFIED
+            if (this.projection.PROJECTION==ProjectionEnum.AITOFF) {
+                var xdiff = cornersXYView[0].vx-cornersXYView[2].vx;
+                var ydiff = cornersXYView[0].vy-cornersXYView[2].vy;
+                var distDiag = Math.sqrt(xdiff*xdiff + ydiff*ydiff);
+                if (distDiag>this.largestDim/5) {
+                    continue;
+                }
+                xdiff = cornersXYView[1].vx-cornersXYView[3].vx;
+                ydiff = cornersXYView[1].vy-cornersXYView[3].vy;
+                distDiag = Math.sqrt(xdiff*xdiff + ydiff*ydiff);
+                if (distDiag>this.largestDim/5) {
+                    continue;
+                }
+            }
+
+            cornersXYView.ipix = ipix;
+            cells.push(cornersXYView);
+        }
+
+        return cells;
+    };
+
+
+
+    // get position in view for a given HEALPix cell
+    View.prototype.getPositionsInView = function(ipix, norder) {
+        var cornersXY = [];
+        var lon, lat;
+        var spVec = new SpatialVector();
+        var nside = Math.pow(2, norder); // TODO : to be modified
+
+
+        var cornersXYView = [];  // will be returned
+        var corners = HealpixCache.corners_nest(ipix, nside);
+
+        for (var k=0; k<4; k++) {
+            spVec.setXYZ(corners[k].x, corners[k].y, corners[k].z);
+
+            // need for frame transformation ?
+            if (this.imageSurvey && this.imageSurvey.cooFrame.system != this.cooFrame.system) {
+                if (this.imageSurvey.cooFrame.system == CooFrameEnum.SYSTEMS.J2000) {
+                    var radec = CooConversion.J2000ToGalactic([spVec.ra(), spVec.dec()]);
+                    lon = radec[0];
+                    lat = radec[1];
+                }
+                else if (this.imageSurvey.cooFrame.system == CooFrameEnum.SYSTEMS.GAL) {
+                    var radec = CooConversion.GalacticToJ2000([spVec.ra(), spVec.dec()]);
+                    lon = radec[0];
+                    lat = radec[1];
+                }
+            }
+            else {
+                lon = spVec.ra();
+                lat = spVec.dec();
+            }
+
+            cornersXY[k] = this.projection.project(lon, lat);
+        }
+
+        if (cornersXY[0] == null ||  cornersXY[1] == null  ||  cornersXY[2] == null ||  cornersXY[3] == null ) {
+            return null;
+        }
+
+
+        for (var k=0; k<4; k++) {
+            cornersXYView[k] = AladinUtils.xyToView(cornersXY[k].X, cornersXY[k].Y, this.width, this.height, this.largestDim, this.zoomFactor);
+        }
+
+        return cornersXYView;
+    };
+
+
+    View.prototype.computeZoomFactor = function(level) {
+        if (level>0) {
+            return AladinUtils.getZoomFactorForAngle(180/Math.pow(1.15, level), this.projectionMethod);
+        }
+        else {
+            return 1 + 0.1*level;
+        }
+    };
+
+    View.prototype.setZoom = function(fovDegrees) {
+        if (fovDegrees<0 || (fovDegrees>180 && ! this.aladin.options.allowFullZoomout)) {
+            return;
+        }
+        var zoomLevel = Math.log(180/fovDegrees)/Math.log(1.15);
+        this.setZoomLevel(zoomLevel);
+    };
+
+    View.prototype.setShowGrid = function(showGrid) {
+        this.showGrid = showGrid;
+        this.requestRedraw();
+    };
+
+
+    View.prototype.setZoomLevel = function(level) {
+        if (this.minFOV || this.maxFOV) {
+            var newFov = doComputeFov(this, this.computeZoomFactor(Math.max(-2, level)));
+            if (this.maxFOV && newFov>this.maxFOV  ||  this.minFOV && newFov<this.minFOV)  {
+                return;
+            }
+        }
+
+        if (this.projectionMethod==ProjectionEnum.SIN) {
+            if (this.aladin.options.allowFullZoomout === true) {
+                // special case for Andreas Wicenec until I fix the problem
+                if (this.width/this.height>2) {
+                    this.zoomLevel = Math.max(-7, level); // TODO : canvas freezes in firefox when max level is small
+                }
+                else if (this.width/this.height<0.5) {
+                    this.zoomLevel = Math.max(-2, level); // TODO : canvas freezes in firefox when max level is small
+                }
+                else {
+                    this.zoomLevel = Math.max(-6, level); // TODO : canvas freezes in firefox when max level is small
+                }
+            }
+            else {
+                this.zoomLevel = Math.max(-2, level); // TODO : canvas freezes in firefox when max level is small
+            }
+        }
+        else {
+            this.zoomLevel = Math.max(-7, level); // TODO : canvas freezes in firefox when max level is small
+        }
+
+
+        this.zoomFactor = this.computeZoomFactor(this.zoomLevel);
+
+        var oldFov = this.fov;
+        this.fov = computeFov(this);
+
+
+        // TODO: event/listener should be better
+        updateFovDiv(this);
+
+        this.computeNorder();
+
+        this.forceRedraw();
+        this.requestRedraw();
+
+        // on avertit les catalogues progressifs
+        if (! this.debounceProgCatOnZoom) {
+            var self = this;
+            this.debounceProgCatOnZoom = Utils.debounce(function() {self.refreshProgressiveCats();}, 300);
+        }
+        this.debounceProgCatOnZoom();
+
+    };
+
+    /**
+     * compute and set the norder corresponding to the current view resolution
+     */
+    View.prototype.computeNorder = function() {
+        var resolution = this.fov / this.largestDim; // in degree/pixel
+        var tileSize = 512; // TODO : read info from HpxImageSurvey.tileSize
+        var nside = HealpixIndex.calculateNSide(3600*tileSize*resolution); // 512 = size of a "tile" image
+        var norder = Math.log(nside)/Math.log(2);
+        norder = Math.max(norder, 1);
+        this.realNorder = norder;
+
+
+        // here, we force norder to 3 (otherwise, the display is "blurry" for too long when zooming in)
+        if (this.fov<=50 && norder<=2) {
+            norder = 3;
+        }
+
+
+        // that happens if we do not wish to display tiles coming from Allsky.[jpg|png]
+        if (this.imageSurvey && norder<=2 && this.imageSurvey.minOrder>2) {
+            norder = this.imageSurvey.minOrder;
+        }
+
+        var overlayNorder  = norder;
+        if (this.imageSurvey && norder>this.imageSurvey.maxOrder) {
+            norder = this.imageSurvey.maxOrder;
+        }
+        if (this.overlayImageSurvey && overlayNorder>this.overlayImageSurvey.maxOrder) {
+            overlayNorder = this.overlayImageSurvey.maxOrder;
+        }
+        // should never happen, as calculateNSide will return something <=HealpixIndex.ORDER_MAX
+        if (norder>HealpixIndex.ORDER_MAX) {
+            norder = HealpixIndex.ORDER_MAX;
+        }
+        if (overlayNorder>HealpixIndex.ORDER_MAX) {
+            overlayNorder = HealpixIndex.ORDER_MAX;
+        }
+
+        this.curNorder = norder;
+        this.curOverlayNorder = overlayNorder;
+    };
+
+    View.prototype.untaintCanvases = function() {
+        this.createCanvases();
+        createListeners(this);
+        this.fixLayoutDimensions();
+    };
+
+    View.prototype.setOverlayImageSurvey = function(overlayImageSurvey, callback) {
+        if (! overlayImageSurvey) {
+            this.overlayImageSurvey = null;
+            this.requestRedraw();
+            return;
+        }
+
+        // reset canvas to "untaint" canvas if needed
+        // we test if the previous base image layer was using CORS or not
+        if ($.support.cors && this.overlayImageSurvey && ! this.overlayImageSurvey.useCors) {
+            this.untaintCanvases();
+        }
+
+        var newOverlayImageSurvey;
+        if (typeof overlayImageSurvey == "string") {
+            newOverlayImageSurvey = HpxImageSurvey.getSurveyFromId(overlayImageSurvey);
+            if ( ! newOverlayImageSurvey) {
+                newOverlayImageSurvey = HpxImageSurvey.getSurveyFromId(HpxImageSurvey.DEFAULT_SURVEY_ID);
+            }
+        }
+        else {
+            newOverlayImageSurvey = overlayImageSurvey;
+        }
+        newOverlayImageSurvey.isReady = false;
+        this.overlayImageSurvey = newOverlayImageSurvey;
+
+        var self = this;
+        newOverlayImageSurvey.init(this, function() {
+            //self.imageSurvey = newImageSurvey;
+            self.computeNorder();
+            newOverlayImageSurvey.isReady = true;
+            self.requestRedraw();
+            self.updateObjectsLookup();
+
+            if (callback) {
+                callback();
+            }
+        });
+    };
+
+    View.prototype.setUnknownSurveyIfNeeded = function() {
+        if (unknownSurveyId) {
+            this.setImageSurvey(unknownSurveyId);
+            unknownSurveyId = undefined;
+        }
+    }
+
+    var unknownSurveyId = undefined;
+    // @param imageSurvey : HpxImageSurvey object or image survey identifier
+    View.prototype.setImageSurvey = function(imageSurvey, callback) {
+        if (! imageSurvey) {
+            return;
+        }
+
+        // reset canvas to "untaint" canvas if needed
+        // we test if the previous base image layer was using CORS or not
+        if ($.support.cors && this.imageSurvey && ! this.imageSurvey.useCors) {
+            this.untaintCanvases();
+        }
+
+        var newImageSurvey;
+        if (typeof imageSurvey == "string") {
+            newImageSurvey = HpxImageSurvey.getSurveyFromId(imageSurvey);
+            if ( ! newImageSurvey) {
+                newImageSurvey = HpxImageSurvey.getSurveyFromId(HpxImageSurvey.DEFAULT_SURVEY_ID);
+                unknownSurveyId = imageSurvey;
+            }
+        }
+        else {
+            newImageSurvey = imageSurvey;
+        }
+
+        // TODO: this is a temporary fix for issue https://github.com/cds-astro/aladin-lite/issues/16
+        // ideally, instead of creating a new TileBuffer object,
+        //  one should remove from TileBuffer all Tile objects still in the download queue qui sont encore dans la download queue
+        this.tileBuffer = new TileBuffer();
+
+        this.downloader.emptyQueue();
+
+        newImageSurvey.isReady = false;
+        this.imageSurvey = newImageSurvey;
+
+        this.projection.reverseLongitude(this.imageSurvey.longitudeReversed);
+
+        var self = this;
+        newImageSurvey.init(this, function() {
+            //self.imageSurvey = newImageSurvey;
+            self.computeNorder();
+            newImageSurvey.isReady = true;
+            self.requestRedraw();
+            self.updateObjectsLookup();
+
+            if (callback) {
+                callback();
+            }
+        });
+    };
+
+    View.prototype.requestRedraw = function() {
+        this.needRedraw = true;
+    };
+
+    View.prototype.changeProjection = function(projectionMethod) {
+        this.projectionMethod = projectionMethod;
+        this.requestRedraw();
+    };
+
+    View.prototype.changeFrame = function(cooFrame) {
+        var oldCooFrame = this.cooFrame;
+        this.cooFrame = cooFrame;
+        // recompute viewCenter
+        if (this.cooFrame.system == CooFrameEnum.SYSTEMS.GAL && this.cooFrame.system != oldCooFrame.system) {
+            var lb = CooConversion.J2000ToGalactic([this.viewCenter.lon, this.viewCenter.lat]);
+            this.viewCenter.lon = lb[0];
+            this.viewCenter.lat = lb[1];
+        }
+        else if (this.cooFrame.system == CooFrameEnum.SYSTEMS.J2000 && this.cooFrame.system != oldCooFrame.system) {
+            var radec = CooConversion.GalacticToJ2000([this.viewCenter.lon, this.viewCenter.lat]);
+            this.viewCenter.lon = radec[0];
+            this.viewCenter.lat = radec[1];
+        }
+
+        this.location.update(this.viewCenter.lon, this.viewCenter.lat, this.cooFrame, true);
+
+        this.requestRedraw();
+    };
+
+    View.prototype.showHealpixGrid = function(show) {
+        this.displayHpxGrid = show;
+        this.requestRedraw();
+    };
+
+    View.prototype.showSurvey = function(show) {
+        this.displaySurvey = show;
+
+        this.requestRedraw();
+    };
+
+    View.prototype.showCatalog = function(show) {
+        this.displayCatalog = show;
+
+        if (!this.displayCatalog) {
+            this.mustClearCatalog = true;
+        }
+        this.requestRedraw();
+    };
+
+    View.prototype.showReticle = function(show) {
+        this.displayReticle = show;
+
+        this.mustRedrawReticle = true;
+        this.requestRedraw();
+    };
+
+    View.prototype.pointTo = function(ra, dec) {
+        ra = parseFloat(ra);
+        dec = parseFloat(dec);
+        if (isNaN(ra) || isNaN(dec)) {
+            return;
+        }
+        if (this.cooFrame.system==CooFrameEnum.SYSTEMS.J2000) {
+            this.viewCenter.lon = ra;
+            this.viewCenter.lat = dec;
+        }
+        else if (this.cooFrame.system==CooFrameEnum.SYSTEMS.GAL) {
+            var lb = CooConversion.J2000ToGalactic([ra, dec]);
+            this.viewCenter.lon = lb[0];
+            this.viewCenter.lat = lb[1];
+        }
+
+        this.location.update(this.viewCenter.lon, this.viewCenter.lat, this.cooFrame, true);
+
+        this.forceRedraw();
+        this.requestRedraw();
+        var self = this;
+        setTimeout(function() {self.refreshProgressiveCats();}, 1000);
+
+    };
+    View.prototype.makeUniqLayerName = function(name) {
+        if (! this.layerNameExists(name)) {
+            return name;
+        }
+        for (var k=1;;++k) {
+            var newName = name + '_' + k;
+            if ( ! this.layerNameExists(newName)) {
+                return newName;
+            }
+        }
+    };
+    View.prototype.layerNameExists = function(name) {
+        var c = this.allOverlayLayers;
+        for (var k=0; k<c.length; k++) {
+            if (name==c[k].name) {
+                return true;
+            }
+        }
+        return false;
+    };
+
+    View.prototype.removeLayers = function() {
+        this.catalogs = [];
+        this.overlays = [];
+        this.mocs = [];
+        this.allOverlayLayers = [];
+        this.requestRedraw();
+    };
+
+    View.prototype.addCatalog = function(catalog) {
+        catalog.name = this.makeUniqLayerName(catalog.name);
+        this.allOverlayLayers.push(catalog);
+        this.catalogs.push(catalog);
+        if (catalog.type=='catalog') {
+            catalog.setView(this);
+        }
+        else if (catalog.type=='progressivecat') {
+            catalog.init(this);
+        }
+    };
+    View.prototype.addOverlay = function(overlay) {
+        overlay.name = this.makeUniqLayerName(overlay.name);
+        this.overlays.push(overlay);
+        this.allOverlayLayers.push(overlay);
+        overlay.setView(this);
+    };
+
+    View.prototype.addMOC = function(moc) {
+        moc.name = this.makeUniqLayerName(moc.name);
+        this.mocs.push(moc);
+        this.allOverlayLayers.push(moc);
+        moc.setView(this);
+    };
+
+    View.prototype.getObjectsInBBox = function(x, y, w, h) {
+        if (w<0) {
+            x = x+w;
+            w = -w;
+        }
+        if (h<0) {
+            y = y+h;
+            h = -h;
+        }
+        var objList = [];
+        var cat, sources, s;
+        if (this.catalogs) {
+            for (var k=0; k<this.catalogs.length; k++) {
+                cat = this.catalogs[k];
+                if (!cat.isShowing) {
+                    continue;
+                }
+                sources = cat.getSources();
+                for (var l=0; l<sources.length; l++) {
+                    s = sources[l];
+                    if (!s.isShowing || !s.x || !s.y) {
+                        continue;
+                    }
+                    if (s.x>=x && s.x<=x+w && s.y>=y && s.y<=y+h) {
+                        objList.push(s);
+                    }
+                }
+            }
+        }
+        return objList;
+
+    };
+
+    // update objLookup, lookup table
+    View.prototype.updateObjectsLookup = function() {
+        this.objLookup = [];
+
+        var cat, sources, s, x, y;
+        if (this.catalogs) {
+            for (var k=0; k<this.catalogs.length; k++) {
+                cat = this.catalogs[k];
+                if (!cat.isShowing) {
+                    continue;
+                }
+                sources = cat.getSources();
+                for (var l=0; l<sources.length; l++) {
+                    s = sources[l];
+                    if (!s.isShowing || !s.x || !s.y) {
+                        continue;
+                    }
+
+                    x = s.x;
+                    y = s.y;
+
+                    if (typeof this.objLookup[x] === 'undefined') {
+                        this.objLookup[x] = [];
+                    }
+                    if (typeof this.objLookup[x][y] === 'undefined') {
+                        this.objLookup[x][y] = [];
+                    }
+                    this.objLookup[x][y].push(s);
+                }
+            }
+        }
+    };
+
+    // return closest object within a radius of maxRadius pixels. maxRadius is an integer
+    View.prototype.closestObjects = function(x, y, maxRadius) {
+
+        // footprint selection code adapted from Fabrizio Giordano dev. from Serco for ESA/ESDC
+        var overlay;
+        var canvas=this.catalogCanvas;
+        var ctx = canvas.getContext("2d");
+        // this makes footprint selection easier as the catch-zone is larger
+        ctx.lineWidth = 6;
+
+        if (this.overlays) {
+            for (var k=0; k<this.overlays.length; k++) {
+                overlay = this.overlays[k];
+                for (var i=0; i<overlay.overlays.length;i++){
+
+                    // test polygons first
+                    var footprint = overlay.overlays[i];
+                    var pointXY = [];
+                    for(var j=0;j<footprint.polygons.length;j++){
+
+                        var xy = AladinUtils.radecToViewXy(footprint.polygons[j][0], footprint.polygons[j][1],
+                                this.projection,
+                                this.cooFrame,
+                                this.width, this.height,
+                                this.largestDim,
+                                this.zoomFactor);
+                        if (! xy) {
+                            continue;
+                        }
+                        pointXY.push({
+                            x: xy.vx,
+                            y: xy.vy
+                        });
+                    }
+                    for(var l=0; l<pointXY.length-1;l++){
+
+                        ctx.beginPath();                        // new segment
+                        ctx.moveTo(pointXY[l].x, pointXY[l].y);     // start is current point
+                        ctx.lineTo(pointXY[l+1].x, pointXY[l+1].y); // end point is next
+                        if (ctx.isPointInStroke(x, y)) {        // x,y is on line?
+                            closest = footprint;
+                            return [closest];
+                        }
+                    }
+                }
+
+                // test Circles
+                for (var i=0; i<overlay.overlay_items.length; i++) {
+                    if (overlay.overlay_items[i] instanceof Circle) {
+                        overlay.overlay_items[i].draw(ctx, this.projection, this.cooFrame, this.width, this.height, this.largestDim, this.zoomFactor, true);
+
+                        if (ctx.isPointInStroke(x, y)) {
+                            closest = overlay.overlay_items[i];
+                            return [closest];
+                        }
+                    }
+                }
+            }
+        }
+
+
+
+
+
+
+        if (!this.objLookup) {
+            return null;
+        }
+        var closest, dist;
+        for (var r=0; r<=maxRadius; r++) {
+            closest = dist = null;
+            for (var dx=-maxRadius; dx<=maxRadius; dx++) {
+                if (! this.objLookup[x+dx]) {
+                    continue;
+                }
+                for (var dy=-maxRadius; dy<=maxRadius; dy++) {
+                    if (this.objLookup[x+dx][y+dy]) {
+                        var d = dx*dx + dy*dy;
+                        if (!closest) {
+                            closest = this.objLookup[x+dx][y+dy];
+                            dist = d;
+                        }
+                        else if (d<dist) {
+                            dist = d;
+                            closest = this.objLookup[x+dx][y+dy];
+                        }
+                    }
+                }
+            }
+            if (closest) {
+                return closest;
+            }
+        }
+        return null;
+    };
+
+    return View;
+})();
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU General Public License version 3.
+//
+// This file is part of Aladin Lite.
+//
+//    Aladin Lite is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, version 3 of the License.
+//
+//    Aladin Lite is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    The GNU General Public License is available in COPYING file
+//    along with Aladin Lite.
+//
+
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File Aladin.js (main class)
+ * Facade to expose Aladin Lite methods
+ *
+ * Author: Thomas Boch[CDS]
+ *
+ *****************************************************************************/
+
+Aladin = (function() {
+
+    // Constructor
+    var Aladin = function(aladinDiv, requestedOptions) {
+        // check that aladinDiv exists, stop immediately otherwise
+        if ($(aladinDiv).length==0) {
+            console.log('Could not find div ' + aladinDiv + '. Aborting creation of Aladin Lite instance');
+            return;
+        }
+
+
+	    var self = this;
+
+	    // if not options was set, try to retrieve them from the query string
+	    if (requestedOptions===undefined) {
+	        requestedOptions = this.getOptionsFromQueryString();
+	    }
+	    requestedOptions = requestedOptions || {};
+
+
+	    // 'fov' option was previsouly called 'zoom'
+	    if ('zoom' in requestedOptions) {
+	        var fovValue = requestedOptions.zoom;
+	        delete requestedOptions.zoom;
+	        requestedOptions.fov = fovValue;
+	    }
+	    // merge with default options
+	    var options = {};
+	    for (var key in Aladin.DEFAULT_OPTIONS) {
+	        if (requestedOptions[key] !== undefined) {
+	            options[key] = requestedOptions[key];
+	        }
+	        else {
+	            options[key] = Aladin.DEFAULT_OPTIONS[key];
+	        }
+	    }
+	    for (var key in requestedOptions) {
+	        if (Aladin.DEFAULT_OPTIONS[key]===undefined) {
+	            options[key] = requestedOptions[key];
+	        }
+	    }
+
+        this.options = options;
+
+        $("<style type='text/css'> .aladin-reticleColor { color: " + this.options.reticleColor + "; font-weight:bold;} </style>").appendTo(aladinDiv);
+
+
+
+		this.aladinDiv = aladinDiv;
+
+        this.reduceDeformations = true;
+
+		// parent div
+		$(aladinDiv).addClass("aladin-container");
+
+
+		var cooFrame = CooFrameEnum.fromString(options.cooFrame, CooFrameEnum.J2000);
+		// locationDiv is the div where we write the position
+		var locationDiv = $('<div class="aladin-location">'
+		                    + (options.showFrame ? '<select class="aladin-frameChoice"><option value="' + CooFrameEnum.J2000.label + '" '
+		                    + (cooFrame==CooFrameEnum.J2000 ? 'selected="selected"' : '') + '>J2000</option><option value="' + CooFrameEnum.J2000d.label + '" '
+		                    + (cooFrame==CooFrameEnum.J2000d ? 'selected="selected"' : '') + '>J2000d</option><option value="' + CooFrameEnum.GAL.label + '" '
+		                    + (cooFrame==CooFrameEnum.GAL ? 'selected="selected"' : '') + '>GAL</option></select>' : '')
+		                    + '<span class="aladin-location-text"></span></div>')
+		                    .appendTo(aladinDiv);
+		// div where FoV value is written
+		var fovDiv = $('<div class="aladin-fov"></div>').appendTo(aladinDiv);
+
+
+		// zoom control
+        if (options.showZoomControl) {
+	          $('<div class="aladin-zoomControl"><a href="#" class="zoomPlus" title="Zoom in">+</a><a href="#" class="zoomMinus" title="Zoom out">&ndash;</a></div>').appendTo(aladinDiv);
+	    }
+
+        // maximize control
+        if (options.showFullscreenControl) {
+            $('<div class="aladin-fullscreenControl aladin-maximize" title="Full screen"></div>')
+                .appendTo(aladinDiv);
+        }
+        this.fullScreenBtn = $(aladinDiv).find('.aladin-fullscreenControl')
+        this.fullScreenBtn.click(function() {
+            self.toggleFullscreen(self.options.realFullscreen);
+        });
+        // react to fullscreenchange event to restore initial width/height (if user pressed ESC to go back from full screen)
+        $(document).on('fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange', function(e) {
+            var fullscreenElt = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement;
+            if (fullscreenElt===null || fullscreenElt===undefined) {
+                self.fullScreenBtn.removeClass('aladin-restore');
+                self.fullScreenBtn.addClass('aladin-maximize');
+                self.fullScreenBtn.attr('title', 'Full screen');
+                $(self.aladinDiv).removeClass('aladin-fullscreen');
+
+                var fullScreenToggledFn = self.callbacksByEventName['fullScreenToggled'];
+                var isInFullscreen = self.fullScreenBtn.hasClass('aladin-restore');
+                (typeof fullScreenToggledFn === 'function') && fullScreenToggledFn(isInFullscreen);
+            }
+        });
+
+
+
+
+
+		// Aladin logo
+		$("<div class='aladin-logo-container'><a href='http://aladin.unistra.fr/' title='Powered by Aladin Lite' target='_blank'><div class='aladin-logo'></div></a></div>").appendTo(aladinDiv);
+
+
+		// we store the boxes
+		this.boxes = [];
+
+        // measurement table
+        this.measurementTable = new MeasurementTable(aladinDiv);
+
+
+
+		var location = new Location(locationDiv.find('.aladin-location-text'));
+
+		// set different options
+		this.view = new View(this, location, fovDiv, cooFrame, options.fov);
+		this.view.setShowGrid(options.showCooGrid);
+
+	    // retrieve available surveys
+        // TODO: replace call with MocServer
+	    $.ajax({
+	        url: "//aladin.unistra.fr/java/nph-aladin.pl",
+	        data: {"frame": "aladinLiteDic"},
+	        method: 'GET',
+	        dataType: 'jsonp', // could this be repaced by json ??
+	        success: function(data) {
+                var map = {};
+                for (var k=0; k<data.length; k++) {
+                    map[data[k].id] = true;
+                }
+                // retrieve existing surveys
+                for (var k=0; k<HpxImageSurvey.SURVEYS.length; k++) {
+                    if (! map[HpxImageSurvey.SURVEYS[k].id]) {
+                        data.push(HpxImageSurvey.SURVEYS[k]);
+                    }
+                }
+	            HpxImageSurvey.SURVEYS = data;
+                self.view.setUnknownSurveyIfNeeded();
+	        },
+	        error: function() {
+	        }
+	    });
+
+	      // layers control panel
+        // TODO : valeur des checkbox en fonction des options
+		// TODO : classe LayerBox
+        if (options.showLayersControl) {
+            var d = $('<div class="aladin-layersControl-container" title="Manage layers"><div class="aladin-layersControl"></div></div>');
+            d.appendTo(aladinDiv);
+
+            var layerBox = $('<div class="aladin-box aladin-layerBox aladin-cb-list"></div>');
+            layerBox.appendTo(aladinDiv);
+
+            this.boxes.push(layerBox);
+
+            // we return false so that the default event is not submitted, and to prevent event bubbling
+            d.click(function() {self.hideBoxes();self.showLayerBox();return false;});
+
+        }
+
+
+        // goto control panel
+        if (options.showGotoControl) {
+            var d = $('<div class="aladin-gotoControl-container" title="Go to position"><div class="aladin-gotoControl"></div></div>');
+            d.appendTo(aladinDiv);
+
+            var gotoBox =
+                $('<div class="aladin-box aladin-gotoBox">' +
+                  '<a class="aladin-closeBtn">&times;</a>' +
+                  '<div style="clear: both;"></div>' +
+                  '<form class="aladin-target-form">Go to: <input type="text" placeholder="Object name/position" /></form></div>');
+            gotoBox.appendTo(aladinDiv);
+            this.boxes.push(gotoBox);
+
+            var input = gotoBox.find('.aladin-target-form input');
+            input.on("paste keydown", function() {
+                $(this).removeClass('aladin-unknownObject'); // remove red border
+            });
+
+            // TODO : classe GotoBox
+            d.click(function() {
+                self.hideBoxes();
+                input.val('');
+                input.removeClass('aladin-unknownObject');
+                gotoBox.show();
+                input.focus();
+
+
+                return false;
+            });
+            gotoBox.find('.aladin-closeBtn').click(function() {self.hideBoxes();return false;});
+        }
+
+        // simbad pointer tool
+        if (options.showSimbadPointerControl) {
+            var d = $('<div class="aladin-simbadPointerControl-container" title="SIMBAD pointer"><div class="aladin-simbadPointerControl"></div></div>');
+            d.appendTo(aladinDiv);
+
+            d.click(function() {
+                self.view.setMode(View.TOOL_SIMBAD_POINTER);
+            });
+        }
+
+        // share control panel
+        if (options.showShareControl) {
+            var d = $('<div class="aladin-shareControl-container" title="Get link for current view"><div class="aladin-shareControl"></div></div>');
+            d.appendTo(aladinDiv);
+
+            var shareBox =
+                $('<div class="aladin-box aladin-shareBox">' +
+                  '<a class="aladin-closeBtn">&times;</a>' +
+                  '<div style="clear: both;"></div>' +
+                  'Link to previewer: <span class="info"></span>' +
+                  '<input type="text" class="aladin-shareInput" />' +
+                  '</div>');
+            shareBox.appendTo(aladinDiv);
+            this.boxes.push(shareBox);
+
+
+            // TODO : classe GotoBox, GenericBox
+            d.click(function() {
+                self.hideBoxes();
+                shareBox.show();
+                var url = self.getShareURL();
+                shareBox.find('.aladin-shareInput').val(url).select();
+                document.execCommand('copy');
+
+                return false;
+            });
+            shareBox.find('.aladin-closeBtn').click(function() {self.hideBoxes();return false;});
+        }
+
+
+        this.gotoObject(options.target);
+
+        if (options.log) {
+            var params = requestedOptions;
+            params['version'] = Aladin.VERSION;
+            Logger.log("startup", params);
+        }
+
+		this.showReticle(options.showReticle);
+
+		if (options.catalogUrls) {
+		    for (var k=0, len=options.catalogUrls.length; k<len; k++) {
+		        this.createCatalogFromVOTable(options.catalogUrls[k]);
+		    }
+		}
+
+		this.setImageSurvey(options.survey);
+		this.view.showCatalog(options.showCatalog);
+
+
+    	var aladin = this;
+    	$(aladinDiv).find('.aladin-frameChoice').change(function() {
+    		aladin.setFrame($(this).val());
+    	});
+    	$('#projectionChoice').change(function() {
+    		aladin.setProjection($(this).val());
+    	});
+
+
+        $(aladinDiv).find('.aladin-target-form').submit(function() {
+            aladin.gotoObject($(this).find('input').val(), function() {
+                $(aladinDiv).find('.aladin-target-form input').addClass('aladin-unknownObject');
+            });
+            return false;
+        });
+
+        var zoomPlus = $(aladinDiv).find('.zoomPlus');
+        zoomPlus.click(function() {
+        	aladin.increaseZoom();
+        	return false;
+        });
+        zoomPlus.bind('mousedown', function(e) {
+            e.preventDefault(); // to prevent text selection
+        });
+
+        var zoomMinus = $(aladinDiv).find('.zoomMinus');
+        zoomMinus.click(function() {
+            aladin.decreaseZoom();
+            return false;
+        });
+        zoomMinus.bind('mousedown', function(e) {
+            e.preventDefault(); // to prevent text selection
+        });
+
+        // go to full screen ?
+        if (options.fullScreen) {
+            window.setTimeout(function() {self.toggleFullscreen(self.options.realFullscreen);}, 1000);
+        }
+
+
+        this.callbacksByEventName = {}; // we store the callback functions (on 'zoomChanged', 'positionChanged', ...) here
+	};
+
+    /**** CONSTANTS ****/
+    Aladin.VERSION = "2022-03-25"; // will be filled by the build.sh script
+
+    Aladin.JSONP_PROXY = "https://alasky.unistra.fr/cgi/JSONProxy";
+    //Aladin.JSONP_PROXY = "https://alaskybis.unistra.fr/cgi/JSONProxy";
+
+
+
+    Aladin.DEFAULT_OPTIONS = {
+        target:                   "0 +0",
+        cooFrame:                 "J2000",
+        survey:                   "P/DSS2/color",
+        fov:                      60,
+        showReticle:              true,
+        showZoomControl:          true,
+        showFullscreenControl:    true,
+        showLayersControl:        true,
+        showGotoControl:          true,
+        showSimbadPointerControl: false,
+        showShareControl:         false,
+        showCatalog:              true, // TODO: still used ??
+        showFrame:                true,
+        showCooGrid:              false,
+        fullScreen:               false,
+        reticleColor:             "rgb(178, 50, 178)",
+        reticleSize:              22,
+        log:                      true,
+        allowFullZoomout:         false,
+        realFullscreen:           false,
+        showAllskyRing:           false,
+        allskyRingColor:          '#c8c8ff',
+        allskyRingWidth:          8,
+        pixelateCanvas:           true
+    };
+
+
+    // realFullscreen: AL div expands not only to the size of its parent, but takes the whole available screen estate
+    Aladin.prototype.toggleFullscreen = function(realFullscreen) {
+        realFullscreen = Boolean(realFullscreen);
+
+        this.fullScreenBtn.toggleClass('aladin-maximize aladin-restore');
+        var isInFullscreen = this.fullScreenBtn.hasClass('aladin-restore');
+        this.fullScreenBtn.attr('title', isInFullscreen ? 'Restore original size' : 'Full screen');
+        $(this.aladinDiv).toggleClass('aladin-fullscreen');
+
+        if (realFullscreen) {
+            // go to "real" full screen mode
+            if (isInFullscreen) {
+                var d = this.aladinDiv;
+
+                if (d.requestFullscreen) {
+                    d.requestFullscreen();
+                }
+                else if (d.webkitRequestFullscreen) {
+                    d.webkitRequestFullscreen();
+                }
+                else if (d.mozRequestFullScreen) { // notice the difference in capitalization for Mozilla functions ...
+                    d.mozRequestFullScreen();
+                }
+                else if (d.msRequestFullscreen) {
+                    d.msRequestFullscreen();
+                }
+            }
+            // exit from "real" full screen mode
+            else {
+                if (document.exitFullscreen) {
+                    document.exitFullscreen();
+                }
+                else if (document.webkitExitFullscreen) {
+                    document.webkitExitFullscreen();
+                }
+                else if (document.mozCancelFullScreen) {
+                    document.mozCancelFullScreen();
+                }
+                else if (document.webkitExitFullscreen) {
+                    document.webkitExitFullscreen();
+                }
+            }
+        }
+
+        this.view.fixLayoutDimensions();
+
+        // force call to zoomChanged callback
+        var fovChangedFn = this.callbacksByEventName['zoomChanged'];
+        (typeof fovChangedFn === 'function') && fovChangedFn(this.view.fov);
+
+        var fullScreenToggledFn = this.callbacksByEventName['fullScreenToggled'];
+        (typeof fullScreenToggledFn === 'function') && fullScreenToggledFn(isInFullscreen);
+    };
+
+    Aladin.prototype.updateSurveysDropdownList = function(surveys) {
+        surveys = surveys.sort(function(a, b) {
+            if (! a.order) {
+                return a.id > b.id;
+            }
+            return a.order && a.order > b.order ? 1 : -1;
+        });
+        var select = $(this.aladinDiv).find('.aladin-surveySelection');
+        select.empty();
+        for (var i=0; i<surveys.length; i++) {
+            var isCurSurvey = this.view.imageSurvey.id==surveys[i].id;
+            select.append($("<option />").attr("selected", isCurSurvey).val(surveys[i].id).text(surveys[i].name));
+        };
+    };
+
+    Aladin.prototype.getOptionsFromQueryString = function() {
+        var options = {};
+        var requestedTarget = $.urlParam('target');
+        if (requestedTarget) {
+            options.target = requestedTarget;
+        }
+        var requestedFrame = $.urlParam('frame');
+        if (requestedFrame && CooFrameEnum[requestedFrame] ) {
+            options.frame = requestedFrame;
+        }
+        var requestedSurveyId = $.urlParam('survey');
+        if (requestedSurveyId && HpxImageSurvey.getSurveyInfoFromId(requestedSurveyId)) {
+            options.survey = requestedSurveyId;
+        }
+        var requestedZoom = $.urlParam('zoom');
+        if (requestedZoom && requestedZoom>0 && requestedZoom<180) {
+            options.zoom = requestedZoom;
+        }
+
+        var requestedShowreticle = $.urlParam('showReticle');
+        if (requestedShowreticle) {
+            options.showReticle = requestedShowreticle.toLowerCase()=='true';
+        }
+
+        var requestedCooFrame =  $.urlParam('cooFrame');
+        if (requestedCooFrame) {
+            options.cooFrame = requestedCooFrame;
+        }
+
+        var requestedFullscreen =  $.urlParam('fullScreen');
+        if (requestedFullscreen !== undefined) {
+            options.fullScreen = requestedFullscreen;
+        }
+
+        return options;
+    };
+
+    // TODO: rename to setFoV
+    //@oldAPI
+	Aladin.prototype.setZoom = function(fovDegrees) {
+		this.view.setZoom(fovDegrees);
+	};
+
+	// @API
+	Aladin.prototype.setFoV = Aladin.prototype.setFov = function(fovDegrees) {
+		this.view.setZoom(fovDegrees);
+	};
+
+    // @API
+    // (experimental) try to adjust the FoV to the given object name. Does nothing if object is not known from Simbad
+	Aladin.prototype.adjustFovForObject = function(objectName) {
+        var self = this;
+		this.getFovForObject(objectName, function(fovDegrees) {
+            self.setFoV(fovDegrees);
+        });
+	};
+
+
+	Aladin.prototype.getFovForObject = function(objectName, callback) {
+        var query = "SELECT galdim_majaxis, V FROM basic JOIN ident ON oid=ident.oidref JOIN allfluxes ON oid=allfluxes.oidref WHERE id='" + objectName + "'";
+        var url = '//simbad.u-strasbg.fr/simbad/sim-tap/sync?query=' + encodeURIComponent(query) + '&request=doQuery&lang=adql&format=json&phase=run';
+
+        var ajax = Utils.getAjaxObject(url, 'GET', 'json', false)
+        ajax.done(function(result) {
+            var defaultFov = 4 / 60; // 4 arcmin
+            var fov = defaultFov;
+
+            if ( 'data' in result && result.data.length>0) {
+                var galdimMajAxis = Utils.isNumber(result.data[0][0]) ? result.data[0][0] / 60.0 : null; // result gives galdim in arcmin
+                var magV = Utils.isNumber(result.data[0][1]) ? result.data[0][1] : null;
+
+                if (galdimMajAxis !== null) {
+                    fov = 2 * galdimMajAxis;
+                }
+                else if (magV !== null) {
+                    if (magV<10) {
+                        fov = 2 * Math.pow(2.0, (6-magV/2.0)) / 60;
+                    }
+                }
+            }
+
+            (typeof callback === 'function') && callback(fov);
+        });
+    };
+
+    Aladin.prototype.setFrame = function(frameName) {
+        if (! frameName) {
+            return;
+        }
+        var newFrame = CooFrameEnum.fromString(frameName, CooFrameEnum.J2000);
+        if (newFrame==this.view.cooFrame)  {
+            return;
+        }
+
+        this.view.changeFrame(newFrame);
+        // màj select box
+        $(this.aladinDiv).find('.aladin-frameChoice').val(newFrame.label);
+    };
+
+	Aladin.prototype.setProjection = function(projectionName) {
+		if (! projectionName) {
+			return;
+		}
+		projectionName = projectionName.toLowerCase();
+		switch(projectionName) {
+			case "aitoff":
+				this.view.changeProjection(ProjectionEnum.AITOFF);
+				break;
+			case "sinus":
+			default:
+				this.view.changeProjection(ProjectionEnum.SIN);
+		}
+	};
+
+    /** point view to a given object (resolved by Sesame) or position
+     * @api
+     *
+     * @param: target; object name or position
+     * @callbackOptions: (optional) the object with key 'success' and/or 'error' containing the success and error callback functions.
+     *
+     */
+    Aladin.prototype.gotoObject = function(targetName, callbackOptions) {
+        var successCallback = errorCallback = undefined;
+        if (typeof callbackOptions === 'object') {
+            if (callbackOptions.hasOwnProperty('success')) {
+                successCallback = callbackOptions.success;
+            }
+            if (callbackOptions.hasOwnProperty('error')) {
+                errorCallback = callbackOptions.error;
+            }
+        }
+        // this is for compatibility reason with the previous method signature which was function(targetName, errorCallback)
+        else if (typeof callbackOptions === 'function') {
+            errorCallback = callbackOptions;
+        }
+
+
+    	var isObjectName = /[a-zA-Z]/.test(targetName);
+
+    	// try to parse as a position
+    	if ( ! isObjectName) {
+    		var coo = new Coo();
+
+			coo.parse(targetName);
+			var lonlat = [coo.lon, coo.lat];
+			if (this.view.cooFrame == CooFrameEnum.GAL) {
+				lonlat = CooConversion.GalacticToJ2000(lonlat);
+			}
+    		this.view.pointTo(lonlat[0], lonlat[1]);
+
+            (typeof successCallback === 'function') && successCallback(this.getRaDec());
+    	}
+    	// ask resolution by Sesame
+    	else {
+	        var self = this;
+	        Sesame.resolve(targetName,
+	                       function(data) { // success callback
+	        					   var ra = data.Target.Resolver.jradeg;
+	        					   var dec = data.Target.Resolver.jdedeg;
+	        					   self.view.pointTo(ra, dec);
+
+                                   (typeof successCallback === 'function') && successCallback(self.getRaDec());
+	                       },
+	                       function(data) { // errror callback
+	                            if (console) {
+	                                console.log("Could not resolve object name " + targetName);
+	                                console.log(data);
+	                            }
+                                (typeof errorCallback === 'function') && errorCallback();
+	                       });
+    	}
+    };
+
+
+
+    /**
+     * go to a given position, expressed in the current coordinate frame
+     *
+     * @API
+     */
+    Aladin.prototype.gotoPosition = function(lon, lat) {
+        var radec;
+        // first, convert to J2000 if needed
+        if (this.view.cooFrame==CooFrameEnum.GAL) {
+            radec = CooConversion.GalacticToJ2000([lon, lat]);
+        }
+        else {
+            radec = [lon, lat];
+        }
+    	this.view.pointTo(radec[0], radec[1]);
+    };
+
+
+    var doAnimation = function(aladin) {
+        var params = aladin.animationParams;
+        if (params==null || ! params['running']) {
+            return;
+        }
+        var now = new Date().getTime();
+        // this is the animation end: set the view to the end position, and call complete callback
+        if (now>params['end']) {
+            aladin.gotoRaDec(params['raEnd'], params['decEnd']);
+
+            if (params['complete']) {
+                params['complete']();
+            }
+
+            return;
+        }
+
+        // compute current position
+        var fraction =  (now-params['start']) / (params['end'] - params['start']);
+        var curPos = intermediatePoint(params['raStart'], params['decStart'], params['raEnd'], params['decEnd'], fraction);
+        curRa =  curPos[0];
+        curDec = curPos[1];
+        //var curRa =  params['raStart'] + (params['raEnd'] - params['raStart']) * (now-params['start']) / (params['end'] - params['start']);
+        //var curDec = params['decStart'] + (params['decEnd'] - params['decStart']) * (now-params['start']) / (params['end'] - params['start']);
+
+        aladin.gotoRaDec(curRa, curDec);
+
+        setTimeout(function() {doAnimation(aladin);}, 50);
+
+    };
+
+    /*
+     * Stop all animations that have been initiated  by animateToRaDec or by zoomToFoV
+     * @API
+     *
+     */
+    Aladin.prototype.stopAnimation =  function() {
+        if (this.zoomAnimationParams) {
+            this.zoomAnimationParams['running'] = false;
+        }
+        if (this.animationParams) {
+            this.animationParams['running']     = false;
+        }
+    }
+
+    /*
+     * animate smoothly from the current position to the given ra, dec
+     *
+     * the total duration (in seconds) of the animation can be given (otherwise set to 5 seconds by default)
+     *
+     * complete: a function to call once the animation has completed
+     *
+     * @API
+     *
+     */
+    Aladin.prototype.animateToRaDec = function(ra, dec, duration, complete) {
+        duration = duration || 5;
+
+        this.animationParams = null;
+
+        var animationParams = {};
+        animationParams['start'] = new Date().getTime();
+        animationParams['end'] = new Date().getTime() + 1000*duration;
+        var raDec = this.getRaDec();
+        animationParams['raStart'] = raDec[0];
+        animationParams['decStart'] = raDec[1];
+        animationParams['raEnd'] = ra;
+        animationParams['decEnd'] = dec;
+        animationParams['complete'] = complete;
+        animationParams['running'] = true;
+
+        this.animationParams = animationParams;
+
+        doAnimation(this);
+    };
+
+    var doZoomAnimation = function(aladin) {
+        var params = aladin.zoomAnimationParams;
+        if (params==null || ! params['running']) {
+            return;
+        }
+        var now = new Date().getTime();
+        // this is the zoom animation end: set the view to the end fov, and call complete callback
+        if (now>params['end']) {
+            aladin.setFoV(params['fovEnd']);
+
+            if (params['complete']) {
+                params['complete']();
+            }
+
+            return;
+        }
+
+        // compute current position
+        var fraction = (now-params['start']) / (params['end'] - params['start']);
+        var curFov =  params['fovStart'] + (params['fovEnd'] - params['fovStart']) * Math.sqrt(fraction);
+
+        aladin.setFoV(curFov);
+
+        setTimeout(function() {doZoomAnimation(aladin);}, 50);
+
+    };
+    /*
+     * zoom smoothly from the current FoV to the given new fov to the given ra, dec
+     *
+     * the total duration (in seconds) of the animation can be given (otherwise set to 5 seconds by default)
+     *
+     * complete: a function to call once the animation has completed
+     *
+     * @API
+     *
+     */
+    Aladin.prototype.zoomToFoV = function(fov, duration, complete) {
+        duration = duration || 5;
+
+        this.zoomAnimationParams = null;
+
+        var zoomAnimationParams = {};
+        zoomAnimationParams['start'] = new Date().getTime();
+        zoomAnimationParams['end'] = new Date().getTime() + 1000*duration;
+        var fovArray = this.getFov();
+        zoomAnimationParams['fovStart'] = Math.max(fovArray[0], fovArray[1]);
+        zoomAnimationParams['fovEnd'] = fov;
+        zoomAnimationParams['complete'] = complete;
+        zoomAnimationParams['running'] = true;
+
+        this.zoomAnimationParams = zoomAnimationParams;
+        doZoomAnimation(this);
+    };
+
+
+
+    /**
+     *  Compute intermediate point between points (lng1, lat1) and (lng2, lat2)
+     *  at distance fraction times the total distance (fraction between 0 and 1)
+     *
+     *  Return intermediate points in degrees
+     *
+     */
+    function intermediatePoint(lng1, lat1, lng2, lat2, fraction) {
+        function degToRad(d) {
+            return d * Math.PI / 180;
+        }
+        function radToDeg(r) {
+            return r * 180 / Math.PI;
+        }
+        var lat1=degToRad(lat1);
+        var lng1=degToRad(lng1);
+        var lat2=degToRad(lat2);
+        var lng2=degToRad(lng2);
+        var d = 2 * Math.asin(
+                    Math.sqrt(Math.pow((Math.sin((lat1 - lat2) / 2)),
+                    2) +
+                    Math.cos(lat1) * Math.cos(lat2) *
+                    Math.pow(Math.sin((lng1-lng2) / 2), 2)));
+        var A = Math.sin((1 - fraction) * d) / Math.sin(d);
+        var B = Math.sin(fraction * d) / Math.sin(d);
+        var x = A * Math.cos(lat1) * Math.cos(lng1) + B *
+            Math.cos(lat2) * Math.cos(lng2);
+        var y = A * Math.cos(lat1) * Math.sin(lng1) + B *
+            Math.cos(lat2) * Math.sin(lng2);
+        var z = A * Math.sin(lat1) + B * Math.sin(lat2);
+        var lon = Math.atan2(y, x);
+        var lat = Math.atan2(z, Math.sqrt(Math.pow(x, 2) +
+             Math.pow(y, 2)));
+
+        return [radToDeg(lon), radToDeg(lat)];
+    };
+
+
+
+
+    /**
+     * get current [ra, dec] position of the center of the view
+     *
+     * @API
+     */
+    Aladin.prototype.getRaDec = function() {
+        if (this.view.cooFrame.system==CooFrameEnum.SYSTEMS.J2000) {
+            return [this.view.viewCenter.lon, this.view.viewCenter.lat];
+        }
+        else {
+            var radec = CooConversion.GalacticToJ2000([this.view.viewCenter.lon, this.view.viewCenter.lat]);
+            return radec;
+
+        }
+    };
+
+
+    /**
+     * point to a given position, expressed as a ra,dec coordinate
+     *
+     * @API
+     */
+    Aladin.prototype.gotoRaDec = function(ra, dec) {
+        this.view.pointTo(ra, dec);
+    };
+
+    Aladin.prototype.showHealpixGrid = function(show) {
+        this.view.showHealpixGrid(show);
+    };
+
+    Aladin.prototype.showSurvey = function(show) {
+        this.view.showSurvey(show);
+    };
+    Aladin.prototype.showCatalog = function(show) {
+        this.view.showCatalog(show);
+    };
+    Aladin.prototype.showReticle = function(show) {
+        this.view.showReticle(show);
+        $('#displayReticle').attr('checked', show);
+    };
+    Aladin.prototype.removeLayers = function() {
+        this.view.removeLayers();
+    };
+
+    // these 3 methods should be merged into a unique "add" method
+    Aladin.prototype.addCatalog = function(catalog) {
+        this.view.addCatalog(catalog);
+    };
+    Aladin.prototype.addOverlay = function(overlay) {
+        this.view.addOverlay(overlay);
+    };
+    Aladin.prototype.addMOC = function(moc) {
+        this.view.addMOC(moc);
+    };
+
+
+
+    // @oldAPI
+    Aladin.prototype.createImageSurvey = function(id, name, rootUrl, cooFrame, maxOrder, options) {
+        return new HpxImageSurvey(id, name, rootUrl, cooFrame, maxOrder, options);
+    };
+
+
+
+    // @api
+    Aladin.prototype.getBaseImageLayer = function() {
+        return this.view.imageSurvey;
+    };
+    // @param imageSurvey : HpxImageSurvey object or image survey identifier
+    // @api
+    // @old
+    Aladin.prototype.setImageSurvey = function(imageSurvey, callback) {
+        this.view.setImageSurvey(imageSurvey, callback);
+        this.updateSurveysDropdownList(HpxImageSurvey.getAvailableSurveys());
+        if (this.options.log) {
+            var id = imageSurvey;
+            if (typeof imageSurvey !== "string") {
+                id = imageSurvey.rootUrl;
+            }
+
+            Logger.log("changeImageSurvey", id);
+        }
+    };
+    // @api
+    Aladin.prototype.setBaseImageLayer = Aladin.prototype.setImageSurvey;
+
+    // @api
+    Aladin.prototype.getOverlayImageLayer = function() {
+        return this.view.overlayImageSurvey;
+    };
+    // @api
+    Aladin.prototype.setOverlayImageLayer = function(imageSurvey, callback) {
+        this.view.setOverlayImageSurvey(imageSurvey, callback);
+    };
+
+
+    Aladin.prototype.increaseZoom = function(step) {
+        if (!step) {
+            step = 5;
+        }
+    	this.view.setZoomLevel(this.view.zoomLevel+step);
+    };
+
+    Aladin.prototype.decreaseZoom = function(step) {
+        if (!step) {
+            step = 5;
+        }
+    	this.view.setZoomLevel(this.view.zoomLevel-step);
+    };
+
+    // @oldAPI
+    Aladin.prototype.createCatalog = function(options) {
+        return A.catalog(options);
+    };
+
+
+    Aladin.prototype.createProgressiveCatalog = function(url, frame, maxOrder, options) {
+        return new ProgressiveCat(url, frame, maxOrder, options);
+    };
+
+    // @oldAPI
+    Aladin.prototype.createSource = function(ra, dec, data) {
+        return new cds.Source(ra, dec, data);
+    };
+    // @oldAPI
+    Aladin.prototype.createMarker = function(ra, dec, options, data) {
+        options = options || {};
+        options['marker'] = true;
+        return new cds.Source(ra, dec, data, options);
+    };
+
+    Aladin.prototype.createOverlay = function(options) {
+        return new Overlay(options);
+    };
+
+    // @oldAPI
+    Aladin.prototype.createFootprintsFromSTCS = function(stcs) {
+        return A.footprintsFromSTCS(stcs);
+    };
+
+    // API
+    A.footprintsFromSTCS = function(stcs) {
+        var footprints = Overlay.parseSTCS(stcs);
+
+        return footprints;
+    }
+
+    // API
+    A.MOCFromURL = function(url, options, successCallback) {
+        var moc = new MOC(options);
+        moc.dataFromFITSURL(url, successCallback);
+
+        return moc;
+    };
+
+    // API
+    A.MOCFromJSON = function(jsonMOC, options) {
+        var moc = new MOC(options);
+        moc.dataFromJSON(jsonMOC);
+
+        return moc;
+    };
+
+
+    // @oldAPI
+    Aladin.prototype.createCatalogFromVOTable = function(url, options) {
+        return A.catalogFromURL(url, options);
+    };
+
+    // TODO: try first without proxy, and then with, if param useProxy not set
+    // API
+    A.catalogFromURL = function(url, options, successCallback, useProxy) {
+        var catalog = A.catalog(options);
+        // TODO: should be self-contained in Catalog class
+        cds.Catalog.parseVOTable(url, function(sources) {
+                catalog.addSources(sources);
+                if (successCallback) {
+                    successCallback(sources);
+                }
+            },
+            catalog.maxNbSources, useProxy,
+            catalog.raField, catalog.decField
+        );
+
+        return catalog;
+    };
+
+    // API
+    // @param target: can be either a string representing a position or an object name, or can be an object with keys 'ra' and 'dec' (values being in decimal degrees)
+    A.catalogFromSimbad = function(target, radius, options, successCallback) {
+        options = options || {};
+        if (! ('name' in options)) {
+            options['name'] = 'Simbad';
+        }
+        var url = URLBuilder.buildSimbadCSURL(target, radius);
+        return A.catalogFromURL(url, options, successCallback, false);
+    };
+
+    // API
+    A.catalogFromNED = function(target, radius, options, successCallback) {
+        options = options || {};
+        if (! ('name' in options)) {
+            options['name'] = 'NED';
+        }
+        var url;
+        if (target && (typeof target  === "object")) {
+            if ('ra' in target && 'dec' in target) {
+                url = URLBuilder.buildNEDPositionCSURL(target.ra, target.dec, radius);
+            }
+        }
+        else {
+    	    var isObjectName = /[a-zA-Z]/.test(target);
+            if (isObjectName)  {
+                url = URLBuilder.buildNEDObjectCSURL(target, radius);
+            }
+            else {
+                var coo = new Coo();
+                coo.parse(target);
+                url = URLBuilder.buildNEDPositionCSURL(coo.lon, coo.lat, radius);
+            }
+        }
+
+        return A.catalogFromURL(url, options, successCallback);
+    };
+
+    // API
+    A.catalogFromVizieR = function(vizCatId, target, radius, options, successCallback) {
+        options = options || {};
+        if (! ('name' in options)) {
+            options['name'] = 'VizieR:' + vizCatId;
+        }
+        var url = URLBuilder.buildVizieRCSURL(vizCatId, target, radius, options);
+
+        return A.catalogFromURL(url, options, successCallback, false);
+    };
+
+    // API
+    A.catalogFromSkyBot = function(ra, dec, radius, epoch, queryOptions, options, successCallback) {
+        queryOptions = queryOptions || {};
+        options = options || {};
+        if (! ('name' in options)) {
+            options['name'] = 'SkyBot';
+        }
+        var url = URLBuilder.buildSkyBotCSURL(ra, dec, radius, epoch, queryOptions);
+        return A.catalogFromURL(url, options, successCallback, false);
+    };
+
+     Aladin.AVAILABLE_CALLBACKS = ['select', 'objectClicked', 'objectHovered', 'footprintClicked', 'footprintHovered', 'positionChanged', 'zoomChanged', 'click', 'mouseMove', 'fullScreenToggled'];
+     // API
+     //
+     // setting callbacks
+     Aladin.prototype.on = function(what, myFunction) {
+         if (Aladin.AVAILABLE_CALLBACKS.indexOf(what)<0) {
+            return;
+         }
+
+         this.callbacksByEventName[what] = myFunction;
+     };
+
+     Aladin.prototype.select = function() {
+         this.fire('selectstart');
+     };
+
+     Aladin.prototype.fire = function(what, params) {
+         if (what==='selectstart') {
+             this.view.setMode(View.SELECT);
+         }
+         else if (what==='selectend') {
+             this.view.setMode(View.PAN);
+             var callbackFn = this.callbacksByEventName['select'];
+             (typeof callbackFn === 'function') && callbackFn(params);
+         }
+     };
+
+     Aladin.prototype.hideBoxes = function() {
+         if (this.boxes) {
+             for (var k=0; k<this.boxes.length; k++) {
+                 this.boxes[k].hide();
+             }
+         }
+     };
+
+     // ?
+     Aladin.prototype.updateCM = function() {
+
+     };
+
+     // TODO : LayerBox (or Stack?) must be extracted as a separate object
+     Aladin.prototype.showLayerBox = function() {
+         var self = this;
+
+         // first, update
+         var layerBox = $(this.aladinDiv).find('.aladin-layerBox');
+         layerBox.empty();
+         layerBox.append('<a class="aladin-closeBtn">&times;</a>' +
+                 '<div style="clear: both;"></div>' +
+                 '<div class="aladin-label">Base image layer</div>' +
+                 '<select class="aladin-surveySelection"></select>' +
+                 '<div class="aladin-cmap">Color map:' +
+                 '<div><select class="aladin-cmSelection"></select><button class="aladin-btn aladin-btn-small aladin-reverseCm" type="button">Reverse</button></div></div>' +
+                 '<div class="aladin-box-separator"></div>' +
+                 '<div class="aladin-label">Overlay layers</div>');
+
+         var cmDiv = layerBox.find('.aladin-cmap');
+
+         // fill color maps options
+         var cmSelect = layerBox.find('.aladin-cmSelection');
+         for (var k=0; k<ColorMap.MAPS_NAMES.length; k++) {
+             cmSelect.append($("<option />").text(ColorMap.MAPS_NAMES[k]));
+         }
+         cmSelect.val(self.getBaseImageLayer().getColorMap().mapName);
+
+
+         // loop over all overlay layers
+         var layers = this.view.allOverlayLayers;
+         var str = '<ul>';
+         for (var k=layers.length-1; k>=0; k--) {
+             var layer = layers[k];
+             var name = layer.name;
+             var checked = '';
+             if (layer.isShowing) {
+                 checked = 'checked="checked"';
+             }
+
+             var tooltipText = '';
+             var iconSvg = '';
+             if (layer.type=='catalog' || layer.type=='progressivecat') {
+                var nbSources = layer.getSources().length;
+                tooltipText = nbSources + ' source' + ( nbSources>1 ? 's' : '');
+
+                iconSvg = AladinUtils.SVG_ICONS.CATALOG;
+            }
+            else if (layer.type=='moc') {
+                tooltipText = 'Coverage: ' + (100*layer.skyFraction()).toFixed(3) + ' % of sky';
+
+                iconSvg = AladinUtils.SVG_ICONS.MOC;
+            }
+            else if (layer.type=='overlay') {
+                iconSvg = AladinUtils.SVG_ICONS.OVERLAY;
+            }
+
+             var rgbColor = $('<div></div>').css('color', layer.color).css('color'); // trick to retrieve the color as 'rgb(,,)' - does not work for named colors :(
+             var labelColor = Color.getLabelColorForBackground(rgbColor);
+
+             // retrieve SVG icon, and apply the layer color
+             var svgBase64 = window.btoa(iconSvg.replace(/FILLCOLOR/g, layer.color));
+             str += '<li><div class="aladin-stack-icon" style=\'background-image: url("data:image/svg+xml;base64,' + svgBase64 + '");\'></div>';
+            str += '<input type="checkbox" ' + checked + ' id="aladin_lite_' + name + '"></input><label for="aladin_lite_' + name + '" class="aladin-layer-label" style="background: ' + layer.color + '; color:' + labelColor + ';" title="' + tooltipText + '">' + name + '</label></li>';
+         }
+         str += '</ul>';
+         layerBox.append(str);
+
+         layerBox.append('<div class="aladin-blank-separator"></div>');
+
+         // gestion du réticule
+         var checked = '';
+         if (this.view.displayReticle) {
+             checked = 'checked="checked"';
+         }
+         var reticleCb = $('<input type="checkbox" ' + checked + ' id="displayReticle" />');
+         layerBox.append(reticleCb).append('<label for="displayReticle">Reticle</label><br/>');
+         reticleCb.change(function() {
+             self.showReticle($(this).is(':checked'));
+         });
+
+         // Gestion grille Healpix
+         checked = '';
+         if (this.view.displayHpxGrid) {
+             checked = 'checked="checked"';
+         }
+         var hpxGridCb = $('<input type="checkbox" ' + checked + ' id="displayHpxGrid"/>');
+         layerBox.append(hpxGridCb).append('<label for="displayHpxGrid">HEALPix grid</label><br/>');
+         hpxGridCb.change(function() {
+             self.showHealpixGrid($(this).is(':checked'));
+         });
+
+
+         layerBox.append('<div class="aladin-box-separator"></div>' +
+              '<div class="aladin-label">Tools</div>');
+         var exportBtn = $('<button class="aladin-btn" type="button">Export view as PNG</button>');
+         layerBox.append(exportBtn);
+         exportBtn.click(function() {
+             self.exportAsPNG();
+         });
+
+                 /*
+                 '<div class="aladin-box-separator"></div>' +
+                 '<div class="aladin-label">Projection</div>' +
+                 '<select id="projectionChoice"><option>SINUS</option><option>AITOFF</option></select><br/>'
+                 */
+
+         layerBox.find('.aladin-closeBtn').click(function() {self.hideBoxes();return false;});
+
+         // update list of surveys
+         this.updateSurveysDropdownList(HpxImageSurvey.getAvailableSurveys());
+         var surveySelection = $(this.aladinDiv).find('.aladin-surveySelection');
+         surveySelection.change(function() {
+             var survey = HpxImageSurvey.getAvailableSurveys()[$(this)[0].selectedIndex];
+             self.setImageSurvey(survey.id, function() {
+                 var baseImgLayer = self.getBaseImageLayer();
+
+                 if (baseImgLayer.useCors) {
+                     // update color map list with current value color map
+                     cmSelect.val(baseImgLayer.getColorMap().mapName);
+                     cmDiv.show();
+
+                     exportBtn.show();
+                 }
+                 else {
+                     cmDiv.hide();
+
+                     exportBtn.hide();
+                 }
+             });
+
+
+
+         });
+
+         //// COLOR MAP management ////////////////////////////////////////////
+         // update color map
+         cmDiv.find('.aladin-cmSelection').change(function() {
+             var cmName = $(this).find(':selected').val();
+             self.getBaseImageLayer().getColorMap().update(cmName);
+         });
+
+         // reverse color map
+         cmDiv.find('.aladin-reverseCm').click(function() {
+             self.getBaseImageLayer().getColorMap().reverse();
+         });
+         if (this.getBaseImageLayer().useCors) {
+             cmDiv.show();
+             exportBtn.show();
+         }
+         else {
+             cmDiv.hide();
+             exportBtn.hide();
+         }
+         layerBox.find('.aladin-reverseCm').parent().attr('disabled', true);
+         //////////////////////////////////////////////////////////////////////
+
+
+         // handler to hide/show overlays
+         $(this.aladinDiv).find('.aladin-layerBox ul input').change(function() {
+             var layerName = ($(this).attr('id').substr(12));
+             var layer = self.layerByName(layerName);
+             if ($(this).is(':checked')) {
+                 layer.show();
+             }
+             else {
+                 layer.hide();
+             }
+         });
+
+         // finally show
+         layerBox.show();
+
+     };
+
+     Aladin.prototype.layerByName = function(name) {
+         var c = this.view.allOverlayLayers;
+         for (var k=0; k<c.length; k++) {
+             if (name==c[k].name) {
+                 return c[k];
+             }
+         }
+         return null;
+     };
+
+     // TODO : integrate somehow into API ?
+     Aladin.prototype.exportAsPNG = function(imgFormat) {
+         var w = window.open();
+         w.document.write('<img src="' + this.getViewDataURL() + '">');
+         w.document.title = 'Aladin Lite snapshot';
+     };
+
+    /**
+     * Return the current view as a data URL (base64-formatted string)
+     * Parameters:
+     * - options (optional): object with attributs
+     *     * format (optional): 'image/png' or 'image/jpeg'
+     *     * width: width in pixels of the image to output
+     *     * height: height in pixels of the image to output
+     *
+     * @API
+    */
+    Aladin.prototype.getViewDataURL = function(options) {
+        var options = options || {};
+        // support for old API signature
+        if (typeof options !== 'object') {
+            var imgFormat = options;
+            options = {format: imgFormat};
+        }
+
+        return this.view.getCanvasDataURL(options.format, options.width, options.height);
+    }
+
+    /**
+     * Return the current view WCS as a key-value dictionary
+     * Can be useful in coordination with getViewDataURL
+     *
+     * @API
+    */
+    Aladin.prototype.getViewWCS = function(options) {
+        var raDec = this.getRaDec();
+        var fov   = this.getFov();
+        // TODO: support for other projection methods than SIN
+        return {
+            NAXIS:     2,
+            NAXIS1:    this.view.width,
+            NAXIS2:    this.view.height,
+            RADECSYS: 'ICRS',
+            CRPIX1:    this.view.width  / 2,
+            CRPIX2:    this.view.height / 2,
+            CRVAL1:    raDec[0],
+            CRVAL2:    raDec[1],
+            CTYPE1:    'RA---SIN',
+            CTYPE2:    'DEC--SIN',
+            CD1_1:     fov[0] / this.view.width,
+            CD1_2:     0.0,
+            CD2_1:     0.0,
+            CD2_2:     fov[1] / this.view.height
+        }
+    }
+
+     /** restrict FOV range
+      * @API
+      * @param minFOV in degrees when zoom in at max
+      * @param maxFOV in degreen when zoom out at max
+     */
+     Aladin.prototype.setFovRange = Aladin.prototype.setFOVRange = function(minFOV, maxFOV) {
+         if (minFOV>maxFOV) {
+             var tmp = minFOV;
+             minFOV = maxFOV;
+             maxFOV = tmp;
+         }
+
+         this.view.minFOV = minFOV;
+         this.view.maxFOV = maxFOV;
+
+     };
+
+     /**
+      * Transform pixel coordinates to world coordinates
+      *
+      * Origin (0,0) of pixel coordinates is at top left corner of Aladin Lite view
+      *
+      * @API
+      *
+      * @param x
+      * @param y
+      *
+      * @return a [ra, dec] array with world coordinates in degrees. Returns undefined is something went wrong
+      *
+      */
+     Aladin.prototype.pix2world = function(x, y) {
+         // this might happen at early stage of initialization
+         if (!this.view) {
+            return undefined;
+         }
+
+         var xy = AladinUtils.viewToXy(x, y, this.view.width, this.view.height, this.view.largestDim, this.view.zoomFactor);
+
+         var radec;
+         try {
+            radec = this.view.projection.unproject(xy.x, xy.y);
+         }
+         catch(e) {
+            return undefined;
+         }
+
+         var res;
+         if (this.view.cooFrame==CooFrameEnum.GAL) {
+             res = CooConversion.GalacticToJ2000([radec.ra, radec.dec]);
+         }
+         else {
+             res =  [radec.ra, radec.dec];
+         }
+
+         return res;
+     };
+
+     /**
+      * Transform world coordinates to pixel coordinates in the view
+      *
+      * @API
+      *
+      * @param ra
+      * @param dec
+      *
+      * @return a [x, y] array with pixel coordinates in the view. Returns null if the projection failed somehow
+      *
+      */
+     Aladin.prototype.world2pix = function(ra, dec) {
+         // this might happen at early stage of initialization
+         if (!this.view) {
+            return;
+         }
+
+         var xy;
+         if (this.view.cooFrame==CooFrameEnum.GAL) {
+             var lonlat = CooConversion.J2000ToGalactic([ra, dec]);
+             xy = this.view.projection.project(lonlat[0], lonlat[1]);
+         }
+         else {
+             xy = this.view.projection.project(ra, dec);
+         }
+         if (xy) {
+             var xyview = AladinUtils.xyToView(xy.X, xy.Y, this.view.width, this.view.height, this.view.largestDim, this.view.zoomFactor);
+             return [xyview.vx, xyview.vy];
+         }
+         else {
+             return null;
+         }
+     };
+
+     /**
+      *
+      * @API
+      *
+      * @param ra
+      * @param nbSteps the number of points to return along each side (the total number of points returned is 4*nbSteps)
+      *
+      * @return set of points along the current FoV with the following format: [[ra1, dec1], [ra2, dec2], ..., [ra_n, dec_n]]
+      *
+      */
+     Aladin.prototype.getFovCorners = function(nbSteps) {
+         // default value: 1
+         if (!nbSteps || nbSteps<1) {
+             nbSteps = 1;
+         }
+
+         var points = [];
+         var x1, y1, x2, y2;
+         for (var k=0; k<4; k++) {
+             x1 = (k==0 || k==3) ? 0 : this.view.width-1;
+             y1 = (k<2) ? 0 : this.view.height-1;
+             x2 = (k<2) ? this.view.width-1 : 0;
+             y2 = (k==1 || k==2) ? this.view.height-1 :0;
+
+             for (var step=0; step<nbSteps; step++) {
+                 points.push(this.pix2world(x1 + step/nbSteps * (x2-x1), y1 + step/nbSteps * (y2-y1)));
+             }
+         }
+
+         return points;
+
+     };
+
+     /**
+      * @API
+      *
+      * @return the current FoV size in degrees as a 2-elements array
+      */
+     Aladin.prototype.getFov = function() {
+         var fovX = this.view.fov;
+         var s = this.getSize();
+         var fovY = s[1] / s[0] * fovX;
+         // TODO : take into account AITOFF projection where fov can be larger than 180
+         fovX = Math.min(fovX, 180);
+         fovY = Math.min(fovY, 180);
+
+         return [fovX, fovY];
+     };
+
+     /**
+      * @API
+      *
+      * @return the size in pixels of the Aladin Lite view
+      */
+     Aladin.prototype.getSize = function() {
+         return [this.view.width, this.view.height];
+     };
+
+     /**
+      * @API
+      *
+      * @return the jQuery object representing the DIV element where the Aladin Lite instance lies
+      */
+     Aladin.prototype.getParentDiv = function() {
+         return $(this.aladinDiv);
+     };
+
+	return Aladin;
+})();
+
+//// New API ////
+// For developers using Aladin lite: all objects should be created through the API,
+// rather than creating directly the corresponding JS objects
+// This facade allows for more flexibility as objects can be updated/renamed harmlessly
+
+//@API
+A.aladin = function(divSelector, options) {
+  return new Aladin($(divSelector)[0], options);
+};
+
+//@API
+// TODO : lecture de properties
+A.imageLayer = function(id, name, rootUrl, options) {
+    return new HpxImageSurvey(id, name, rootUrl, null, null, options);
+};
+
+// @API
+A.source = function(ra, dec, data, options) {
+    return new cds.Source(ra, dec, data, options);
+};
+
+// @API
+A.marker = function(ra, dec, options, data) {
+    options = options || {};
+    options['marker'] = true;
+    return A.source(ra, dec, data, options);
+};
+
+// @API
+A.polygon = function(raDecArray) {
+    var l = raDecArray.length;
+    if (l>0) {
+        // close the polygon if needed
+        if (raDecArray[0][0]!=raDecArray[l-1][0] || raDecArray[0][1]!=raDecArray[l-1][1]) {
+            raDecArray.push([raDecArray[0][0], raDecArray[0][1]]);
+        }
+    }
+    return new Footprint(raDecArray);
+};
+
+//@API
+A.polyline = function(raDecArray, options) {
+    return new Polyline(raDecArray, options);
+};
+
+
+// @API
+A.circle = function(ra, dec, radiusDeg, options) {
+    return new Circle([ra, dec], radiusDeg, options);
+};
+
+// @API
+A.graphicOverlay = function(options) {
+    return new Overlay(options);
+};
+
+// @API
+A.catalog = function(options) {
+    return new cds.Catalog(options);
+};
+
+// @API
+A.catalogHiPS = function(rootURL, options) {
+    return new ProgressiveCat(rootURL, null, null, options);
+};
+
+// @API
+/*
+ * return a Box GUI element to insert content
+ */
+Aladin.prototype.box = function(options) {
+    var box = new Box(options);
+    box.$parentDiv.appendTo(this.aladinDiv);
+
+    return box;
+};
+
+// @API
+/*
+ * show popup at ra, dec position with given title and content
+ */
+Aladin.prototype.showPopup = function(ra, dec, title, content) {
+    this.view.catalogForPopup.removeAll();
+    var marker = A.marker(ra, dec, {popupTitle: title, popupDesc: content, useMarkerDefaultIcon: false});
+    this.view.catalogForPopup.addSources(marker);
+    this.view.catalogForPopup.show();
+
+    this.view.popup.setTitle(title);
+    this.view.popup.setText(content);
+    this.view.popup.setSource(marker);
+    this.view.popup.show();
+};
+
+// @API
+/*
+ * hide popup
+ */
+Aladin.prototype.hidePopup = function() {
+    this.view.popup.hide();
+};
+
+// @API
+/*
+ * return a URL allowing to share the current view
+ */
+Aladin.prototype.getShareURL = function() {
+    var radec = this.getRaDec();
+    var coo = new Coo();
+    coo.prec = 7;
+    coo.lon = radec[0];
+    coo.lat = radec[1];
+
+    return 'http://aladin.unistra.fr/AladinLite/?target=' + encodeURIComponent(coo.format('s')) +
+           '&fov=' + this.getFov()[0].toFixed(2) + '&survey=' + encodeURIComponent(this.getBaseImageLayer().id || this.getBaseImageLayer().rootUrl);
+};
+
+// @API
+/*
+ * return, as a string, the HTML embed code
+ */
+Aladin.prototype.getEmbedCode = function() {
+    var radec = this.getRaDec();
+    var coo = new Coo();
+    coo.prec = 7;
+    coo.lon = radec[0];
+    coo.lat = radec[1];
+
+    var survey = this.getBaseImageLayer().id;
+    var fov = this.getFov()[0];
+    var s = '';
+    s += '<link rel="stylesheet" href="http://aladin.unistra.fr/AladinLite/api/v2/latest/aladin.min.css" />\n';
+    s += '<script type="text/javascript" src="//code.jquery.com/jquery-1.9.1.min.js" charset="utf-8"></script>\n';
+    s += '<div id="aladin-lite-div" style="width:400px;height:400px;"></div>\n';
+    s += '<script type="text/javascript" src="http://aladin.unistra.fr/AladinLite/api/v2/latest/aladin.min.js" charset="utf-8"></script>\n';
+    s += '<script type="text/javascript">\n';
+    s += 'var aladin = A.aladin("#aladin-lite-div", {survey: "' + survey + 'P/DSS2/color", fov: ' + fov.toFixed(2) + ', target: "' + coo.format('s') + '"});\n';
+    s += '</script>';
+    return s;
+};
+
+// @API
+/*
+ * Creates remotely a HiPS from a FITS image URL and displays it
+ */
+Aladin.prototype.displayFITS = function(url, options, successCallback, errorCallback) {
+    options = options || {};
+    var data = {url: url};
+    if (options.color) {
+        data.color = true;
+    }
+    if (options.outputFormat) {
+        data.format = options.outputFormat;
+    }
+    if (options.order) {
+        data.order = options.order;
+    }
+    if (options.nocache) {
+        data.nocache = options.nocache;
+    }
+    var self = this;
+    $.ajax({
+        url: 'https://alasky.unistra.fr/cgi/fits2HiPS',
+        data: data,
+        method: 'GET',
+        dataType: 'json',
+        success: function(response) {
+            if (response.status!='success') {
+                console.error('An error occured: ' + response.message);
+                if (errorCallback) {
+                    errorCallback(response.message);
+                }
+                return;
+            }
+            var label = options.label || "FITS image";
+            var meta = response.data.meta;
+            self.setOverlayImageLayer(self.createImageSurvey(label, label, response.data.url, "equatorial", meta.max_norder, {imgFormat: 'png'}));
+            var transparency = (options && options.transparency) || 1.0;
+            self.getOverlayImageLayer().setAlpha(transparency);
+
+            var executeDefaultSuccessAction = true;
+            if (successCallback) {
+                executeDefaultSuccessAction = successCallback(meta.ra, meta.dec, meta.fov);
+            }
+            if (executeDefaultSuccessAction===true) {
+                self.gotoRaDec(meta.ra, meta.dec);
+                self.setFoV(meta.fov);
+            }
+
+        }
+    });
+
+};
+
+// @API
+/*
+ * Creates remotely a HiPS from a JPEG or PNG image with astrometry info
+ * and display it
+ */
+Aladin.prototype.displayJPG = Aladin.prototype.displayPNG = function(url, options, successCallback, errorCallback) {
+    options = options || {};
+    options.color = true;
+    options.label = "JPG/PNG image";
+    options.outputFormat = 'png';
+    this.displayFITS(url, options, successCallback, errorCallback);
+};
+
+Aladin.prototype.setReduceDeformations = function(reduce) {
+    this.reduceDeformations = reduce;
+    this.view.requestRedraw();
+}
+
+
+
+// conservé pour compatibilité avec existant
+// @oldAPI
+if ($) {
+    $.aladin = A.aladin;
+}
+
+// TODO: callback function onAladinLiteReady
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index 74b5e053450a48a6bdb4d71aad648e7af821975c..0000000000000000000000000000000000000000
--- a/src/App.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.App {
-  text-align: center;
-}
-
-.App-logo {
-  height: 40vmin;
-  pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
-  .App-logo {
-    animation: App-logo-spin infinite 20s linear;
-  }
-}
-
-.App-header {
-  background-color: #282c34;
-  min-height: 100vh;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  font-size: calc(10px + 2vmin);
-  color: white;
-}
-
-.App-link {
-  color: #61dafb;
-}
-
-@keyframes App-logo-spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}
diff --git a/src/App.tsx b/src/App.tsx
index a53698aab3c66049c61980112dd0109dd2cd0845..3ecc94a5abd9305ce7c620a73bd1d615fa46f8cf 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,24 +1,44 @@
-import React from 'react';
-import logo from './logo.svg';
-import './App.css';
+import React, { useState } from 'react';
+import SkyView from './SkyView';
+
+// TODO: Load sources from somewhere else
+const a = A.source(0, 0, { hello: "World", size: 42 });
+const catalogA = A.catalog({ name: "A", sourceSize: 20, color: "#fff", onClick: "showPopup" });
+catalogA.addSources([a]);
+
+const b = A.source(0, 2, { hello: "universe", size: 42 });
+const catalogB = A.catalog({ name: "B", sourceSize: 20, color: "#f00", onClick: "showPopup" });
+catalogB.addSources([b]);
 
 function App() {
+
+
+  const [catalogs, setCatalogs] = useState([catalogA]);
+
+  function handleClickA() {
+    console.log("A & B");
+    setCatalogs([catalogA, catalogB])
+  }
+  function handleClickB() {
+    console.log("A");
+    setCatalogs([catalogA]);
+  }
+
+  function handleClickC() {
+    catalogA.addSources(A.source(Math.random(), Math.random(), { hello: "World", size: 42 }));
+    setCatalogs([catalogA]);
+  }
+
+  function handleViewChanged(ra: number, dec: number, fov: number) {
+    console.log(ra, dec, fov)
+  }
+
   return (
-    <div className="App">
-      <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.tsx</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
-      </header>
+    <div>
+      <SkyView catalogs={catalogs} onViewChanged={handleViewChanged} />
+      <button onClick={handleClickA}>Add B</button>
+      <button onClick={handleClickB}>Remove B</button>
+      <button onClick={handleClickC}>Add new sources</button>
     </div>
   );
 }
diff --git a/src/SkyView.tsx b/src/SkyView.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..49b16487381298ed20320f3517d7479f68154bd2
--- /dev/null
+++ b/src/SkyView.tsx
@@ -0,0 +1,84 @@
+import React, { CSSProperties, useEffect, useRef, useState } from "react";
+
+type Props = {
+  catalogs: AladinCatalog[],
+  onViewChanged: (ra: number, dec: number, fov: number) => void
+};
+
+const style: CSSProperties = { width: 800, height: 600, background: "#AAA" };
+
+// TODO: Add event listeners for dragging -> updating the view
+const SkyView = (props: Props) => {
+
+  const { catalogs, onViewChanged } = props;
+
+  const ref = useRef<HTMLDivElement>(null);
+  const [instance, setInstance] = useState<AladinInstance | null>(null);
+
+
+
+
+  useEffect(() => {
+    if (ref.current) {
+      const el = ref.current;
+      setInstance(A.aladin(`#${el.id}`));
+      console.error("sky view mounted");
+
+      return () => {
+        console.error("sky view unmounted");
+        // TODO: remove listeners?
+        // Check if everything is correctly cleared.
+
+        // Destroy aladin SkyView
+        el.innerHTML = "";
+        setInstance(null);
+      }
+    }
+
+  }, []);
+
+  /**
+   * Re add the catalog whenever the instance is changed
+   */
+  useEffect(() => {
+    if (instance) {
+
+      console.log(catalogs);
+
+      for (let catalog of catalogs) {
+        instance.addCatalog(catalog);
+      }
+
+      instance.on("positionChanged", (_) => {
+        const [ra, dec] = instance?.getRaDec();
+        const fov = Math.max(...instance?.getFov());
+        onViewChanged(ra, dec, fov);
+      });
+
+      // TODO: add opZoomChanged
+
+      console.log("catalogs added");
+      return () => {
+        // This does not clear the layers in the `Manage Layers` toolbox
+        instance.removeLayers();
+        console.log("catalogs removed");
+      }
+    }
+
+  }, [catalogs, instance, onViewChanged]);
+
+  function handleDragEnd() {
+    console.log(instance?.getRaDec(), instance?.getFov());
+  }
+
+
+  return (
+    <div
+      onMouseUp={handleDragEnd}
+      onMouseLeave={handleDragEnd}
+      onWheel={handleDragEnd}
+       ref={ref} style={style} id="SkyView"/>
+  );
+}
+
+export default SkyView;
diff --git a/src/aladin.d.ts b/src/aladin.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7c7ef2c001b3e99db118348294232c59f3ff8cdf
--- /dev/null
+++ b/src/aladin.d.ts
@@ -0,0 +1,101 @@
+// Aladin SkyView Lite is declared as `A` in the global scope
+
+type AladinColorMaps = "cubehelix" | "eosb" | "rainbow" | "grayscale" | "native";
+
+interface AladinImageLayer {
+  // TODO: What are it's properties?
+}
+
+// TODO: Fix callback function declaration
+interface AladinCallback {
+  success: (raDec: string) => void;
+  error: (err?: any) => void;
+}
+
+interface AladinHipsImageFormat {
+  imgFormat: "jpg" | "png";
+}
+
+interface AladinOptions {
+  target?: string;
+  cooFrame?: string;
+  survey?: string;
+  fov?: number;
+  showReticle?: boolean;
+  showZoomControl?: boolean;
+  showFullscreenControl?: boolean;
+  showLayersControl?: boolean;
+  showGotoControl?: boolean;
+  showShareControl?: boolean;
+  showSimbadPointerControl?: boolean;
+  showFrame?: boolean;
+  fullScreen?: boolean;
+  reticleColor?: string;
+  reicleSize?: number;
+};
+
+
+interface AladinSource {
+
+}
+
+interface AladinCatalog {
+  readonly addSources: (sources: AladinSource | AladinSource[]) => void;
+  readonly remove: (source: AladinSource) => void;
+}
+
+interface AladinInstance {
+  readonly getRaDec: () => [number, number];
+  readonly getSize: () => [number, number];
+  readonly getFov: () => [number, number];
+  readonly getFovCorners: (nbSteps?: number) => [number, number][];
+  readonly setFov: (degrees: number) => void;
+  readonly adjustFovForObject: (target: string) => void;
+  readonly setFovRange: (minFov: number, maxFov: number) => void;
+  readonly gotoRaDec: (ra: number, dec: number) => void;
+  readonly gotoObject: (target: string, callback?: AladinCallback) => void;
+  readonly setImageSurvey: (hhipsID: string, hipsName: string, hipsBaseUrl: string, hipsFrame: "equatorial" | "galactic", hipsMaxOrder: number, format: AladinHipsImageFormat) => void;
+  readonly getBaseImageLayer: () => AladinImageLayer;
+  readonly getColorMap: () => AladinColorMaps;
+  readonly displayFITS: (fitsUrl: string) => void;
+  readonly addCatalog: (catalog: AladinCatalog) => void;
+  readonly removeLayers: () => void;
+  // TODO: check if listener can be "cancelled"
+  readonly on: (type: "objectHovered" | "objectClicked" | "positionChanged", handler: (obj: any) => void) => void;
+};
+
+interface AladinCatalogOptions {
+  name?: string;
+  shape?: "circle" | "plus" | "rhomb" | "cross" | "triangle" | "square" | HTMLCanvasElement; // JPEG/PNG also supported, but how?
+  color?: string;
+  sourceSize?: number;
+  raField?: string;
+  decField?: string;
+  labelColumn?: string;
+  labelColor?: string;
+  labelFont?: string;
+  onClick?: "showTable" | "showPopup";
+  limit?: number;
+}
+
+type AladinCatalogCallback = (sources: AladinSource[]) => void;
+
+interface Aladin {
+  readonly aladin: (id: string, options?: AladinOptions) => AladinInstance;
+  readonly catalog: (options?: AladinCatalogOptions) => AladinCatalog;
+  readonly source: (ra: number, dec: number, data: any) => AladinSource;
+  // TODO: add option info
+  readonly catalogFromUrl: (url: string, options?: AladinCatalogOptions, successCallback: AladinCatalogCallback, useProxy?: boolean) => AladinCatalog;
+  // TODO Add other catalog methods
+  // catalogFromSimbad(<target>, <radius-in-degrees>, <catalog-options>?, <successCallback>?)
+  // A.catalogFromVizieR(<vizier-cat-id>, <target>, <radius-in-deg>, <cat-options>?, <successCallback>?)
+  // A.catalogFromNED(<target>, <radius-in-degrees>, <catalog-options>?, <successCallback>?)
+  // Additional optional query options can be specified as a keyword/value dictionary, eg: {"-loc": 500, "-filter": 0}
+  // A.catalogFromSkyBot(<ra>, <dec>, <radius>, <epoch>, <query-options>?, <cat-options>?, <successCallback>?)
+  // Markers?
+  // A.marker(ra, dec, {popupTitle: <title of the popup>, popupDesc: <text (possibly HTML) for the popup>})
+
+  // TODO: Add the final functions
+};
+
+declare const A: Aladin;
diff --git a/src/index.css b/src/index.css
index ec2585e8c0bb8188184ed1e0703c4c8f2a8419b0..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,13 +0,0 @@
-body {
-  margin: 0;
-  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
-    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
-    sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-}
-
-code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
-    monospace;
-}
diff --git a/src/index.tsx b/src/index.tsx
index 032464fb6ec40a523899b8c8a593242f3108a420..28fcee7fb74d4b682e13034c7b7086e9696d69e0 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,7 +1,8 @@
 import React from 'react';
 import ReactDOM from 'react-dom/client';
-import './index.css';
 import App from './App';
+import './index.css';
+import "./normalize.css";
 import reportWebVitals from './reportWebVitals';
 
 const root = ReactDOM.createRoot(
diff --git a/src/logo.svg b/src/logo.svg
deleted file mode 100644
index 9dfc1c058cebbef8b891c5062be6f31033d7d186..0000000000000000000000000000000000000000
--- a/src/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
\ No newline at end of file
diff --git a/src/normalize.css b/src/normalize.css
new file mode 100644
index 0000000000000000000000000000000000000000..1a9fc1b8214e9817cb4692f83cd8f1ca3ed48db5
--- /dev/null
+++ b/src/normalize.css
@@ -0,0 +1,349 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+   ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+ html {
+    line-height: 1.15; /* 1 */
+    -webkit-text-size-adjust: 100%; /* 2 */
+  }
+
+  /* Sections
+     ========================================================================== */
+
+  /**
+   * Remove the margin in all browsers.
+   */
+
+  body {
+    margin: 0;
+  }
+
+  /**
+   * Render the `main` element consistently in IE.
+   */
+
+  main {
+    display: block;
+  }
+
+  /**
+   * Correct the font size and margin on `h1` elements within `section` and
+   * `article` contexts in Chrome, Firefox, and Safari.
+   */
+
+  h1 {
+    font-size: 2em;
+    margin: 0.67em 0;
+  }
+
+  /* Grouping content
+     ========================================================================== */
+
+  /**
+   * 1. Add the correct box sizing in Firefox.
+   * 2. Show the overflow in Edge and IE.
+   */
+
+  hr {
+    box-sizing: content-box; /* 1 */
+    height: 0; /* 1 */
+    overflow: visible; /* 2 */
+  }
+
+  /**
+   * 1. Correct the inheritance and scaling of font size in all browsers.
+   * 2. Correct the odd `em` font sizing in all browsers.
+   */
+
+  pre {
+    font-family: monospace, monospace; /* 1 */
+    font-size: 1em; /* 2 */
+  }
+
+  /* Text-level semantics
+     ========================================================================== */
+
+  /**
+   * Remove the gray background on active links in IE 10.
+   */
+
+  a {
+    background-color: transparent;
+  }
+
+  /**
+   * 1. Remove the bottom border in Chrome 57-
+   * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+   */
+
+  abbr[title] {
+    border-bottom: none; /* 1 */
+    text-decoration: underline; /* 2 */
+    text-decoration: underline dotted; /* 2 */
+  }
+
+  /**
+   * Add the correct font weight in Chrome, Edge, and Safari.
+   */
+
+  b,
+  strong {
+    font-weight: bolder;
+  }
+
+  /**
+   * 1. Correct the inheritance and scaling of font size in all browsers.
+   * 2. Correct the odd `em` font sizing in all browsers.
+   */
+
+  code,
+  kbd,
+  samp {
+    font-family: monospace, monospace; /* 1 */
+    font-size: 1em; /* 2 */
+  }
+
+  /**
+   * Add the correct font size in all browsers.
+   */
+
+  small {
+    font-size: 80%;
+  }
+
+  /**
+   * Prevent `sub` and `sup` elements from affecting the line height in
+   * all browsers.
+   */
+
+  sub,
+  sup {
+    font-size: 75%;
+    line-height: 0;
+    position: relative;
+    vertical-align: baseline;
+  }
+
+  sub {
+    bottom: -0.25em;
+  }
+
+  sup {
+    top: -0.5em;
+  }
+
+  /* Embedded content
+     ========================================================================== */
+
+  /**
+   * Remove the border on images inside links in IE 10.
+   */
+
+  img {
+    border-style: none;
+  }
+
+  /* Forms
+     ========================================================================== */
+
+  /**
+   * 1. Change the font styles in all browsers.
+   * 2. Remove the margin in Firefox and Safari.
+   */
+
+  button,
+  input,
+  optgroup,
+  select,
+  textarea {
+    font-family: inherit; /* 1 */
+    font-size: 100%; /* 1 */
+    line-height: 1.15; /* 1 */
+    margin: 0; /* 2 */
+  }
+
+  /**
+   * Show the overflow in IE.
+   * 1. Show the overflow in Edge.
+   */
+
+  button,
+  input { /* 1 */
+    overflow: visible;
+  }
+
+  /**
+   * Remove the inheritance of text transform in Edge, Firefox, and IE.
+   * 1. Remove the inheritance of text transform in Firefox.
+   */
+
+  button,
+  select { /* 1 */
+    text-transform: none;
+  }
+
+  /**
+   * Correct the inability to style clickable types in iOS and Safari.
+   */
+
+  button,
+  [type="button"],
+  [type="reset"],
+  [type="submit"] {
+    -webkit-appearance: button;
+  }
+
+  /**
+   * Remove the inner border and padding in Firefox.
+   */
+
+  button::-moz-focus-inner,
+  [type="button"]::-moz-focus-inner,
+  [type="reset"]::-moz-focus-inner,
+  [type="submit"]::-moz-focus-inner {
+    border-style: none;
+    padding: 0;
+  }
+
+  /**
+   * Restore the focus styles unset by the previous rule.
+   */
+
+  button:-moz-focusring,
+  [type="button"]:-moz-focusring,
+  [type="reset"]:-moz-focusring,
+  [type="submit"]:-moz-focusring {
+    outline: 1px dotted ButtonText;
+  }
+
+  /**
+   * Correct the padding in Firefox.
+   */
+
+  fieldset {
+    padding: 0.35em 0.75em 0.625em;
+  }
+
+  /**
+   * 1. Correct the text wrapping in Edge and IE.
+   * 2. Correct the color inheritance from `fieldset` elements in IE.
+   * 3. Remove the padding so developers are not caught out when they zero out
+   *    `fieldset` elements in all browsers.
+   */
+
+  legend {
+    box-sizing: border-box; /* 1 */
+    color: inherit; /* 2 */
+    display: table; /* 1 */
+    max-width: 100%; /* 1 */
+    padding: 0; /* 3 */
+    white-space: normal; /* 1 */
+  }
+
+  /**
+   * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+   */
+
+  progress {
+    vertical-align: baseline;
+  }
+
+  /**
+   * Remove the default vertical scrollbar in IE 10+.
+   */
+
+  textarea {
+    overflow: auto;
+  }
+
+  /**
+   * 1. Add the correct box sizing in IE 10.
+   * 2. Remove the padding in IE 10.
+   */
+
+  [type="checkbox"],
+  [type="radio"] {
+    box-sizing: border-box; /* 1 */
+    padding: 0; /* 2 */
+  }
+
+  /**
+   * Correct the cursor style of increment and decrement buttons in Chrome.
+   */
+
+  [type="number"]::-webkit-inner-spin-button,
+  [type="number"]::-webkit-outer-spin-button {
+    height: auto;
+  }
+
+  /**
+   * 1. Correct the odd appearance in Chrome and Safari.
+   * 2. Correct the outline style in Safari.
+   */
+
+  [type="search"] {
+    -webkit-appearance: textfield; /* 1 */
+    outline-offset: -2px; /* 2 */
+  }
+
+  /**
+   * Remove the inner padding in Chrome and Safari on macOS.
+   */
+
+  [type="search"]::-webkit-search-decoration {
+    -webkit-appearance: none;
+  }
+
+  /**
+   * 1. Correct the inability to style clickable types in iOS and Safari.
+   * 2. Change font properties to `inherit` in Safari.
+   */
+
+  ::-webkit-file-upload-button {
+    -webkit-appearance: button; /* 1 */
+    font: inherit; /* 2 */
+  }
+
+  /* Interactive
+     ========================================================================== */
+
+  /*
+   * Add the correct display in Edge, IE 10+, and Firefox.
+   */
+
+  details {
+    display: block;
+  }
+
+  /*
+   * Add the correct display in all browsers.
+   */
+
+  summary {
+    display: list-item;
+  }
+
+  /* Misc
+     ========================================================================== */
+
+  /**
+   * Add the correct display in IE 10+.
+   */
+
+  template {
+    display: none;
+  }
+
+  /**
+   * Add the correct display in IE 10.
+   */
+
+  [hidden] {
+    display: none;
+  }
diff --git a/tsconfig.json b/tsconfig.json
index a273b0cfc0e965c35524e3cd0d3574cbe1ad2d0d..7b1d3c6f511a1d2f86a3ac018d7a6e1febd09909 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -18,7 +18,7 @@
     "resolveJsonModule": true,
     "isolatedModules": true,
     "noEmit": true,
-    "jsx": "react-jsx"
+    "jsx": "react"
   },
   "include": [
     "src"