Image.js

  1. // Copyright 2013 - UDS/CNRS
  2. // The Aladin Lite program is distributed under the terms
  3. // of the GNU General Public License version 3.
  4. //
  5. // This file is part of Aladin Lite.
  6. //
  7. // Aladin Lite is free software: you can redistribute it and/or modify
  8. // it under the terms of the GNU General Public License as published by
  9. // the Free Software Foundation, version 3 of the License.
  10. //
  11. // Aladin Lite is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU General Public License for more details.
  15. //
  16. // The GNU General Public License is available in COPYING file
  17. // along with Aladin Lite.
  18. //
  19. /******************************************************************************
  20. * Aladin Lite project
  21. *
  22. * File Image
  23. *
  24. * Authors: Matthieu Baumann [CDS]
  25. *
  26. *****************************************************************************/
  27. import { ColorCfg } from "./ColorCfg.js";
  28. import { Aladin } from "./Aladin.js";
  29. import { Utils } from "./Utils";
  30. import { AVM } from "./libs/avm.js";
  31. import { HiPS } from "./HiPS.js";
  32. /**
  33. * @typedef {Object} WCS
  34. *
  35. * {@link https://ui.adsabs.harvard.edu/abs/2002A%26A...395.1077C/abstract|FITS (Paper II)}, Calabretta, M. R., and Greisen, E. W., Astronomy & Astrophysics, 395, 1077-1122, 2002
  36. *
  37. * @property {number} [NAXIS]
  38. * @property {string} CTYPE1
  39. * @property {string} [CTYPE2]
  40. * @property {number} [LONPOLE]
  41. * @property {number} [LATPOLE]
  42. * @property {number} [CRVAL1]
  43. * @property {number} [CRVAL2]
  44. * @property {number} [CRPIX1]
  45. * @property {number} [CRPIX2]
  46. * @property {string} [CUNIT1] - e.g. 'deg'
  47. * @property {string} [CUNIT2] - e.g. 'deg'
  48. * @property {number} [CD1_1]
  49. * @property {number} [CD1_2]
  50. * @property {number} [CD2_1]
  51. * @property {number} [CD2_2]
  52. * @property {number} [PC1_1]
  53. * @property {number} [PC1_2]
  54. * @property {number} [PC2_1]
  55. * @property {number} [PC2_2]
  56. * @property {number} [CDELT1]
  57. * @property {number} [CDELT2]
  58. * @property {number} [NAXIS1]
  59. * @property {number} [NAXIS2]
  60. */
  61. /**
  62. * @typedef {Object} ImageOptions
  63. *
  64. * @property {string} [name] - A human-readable name for the FITS image
  65. * @property {Function} [successCallback] - A callback executed when the FITS has been loaded
  66. * @property {Function} [errorCallback] - A callback executed when the FITS could not be loaded
  67. * @property {number} [opacity=1.0] - Opacity of the survey or image (value between 0 and 1).
  68. * @property {string} [colormap="native"] - The colormap configuration for the survey or image.
  69. * @property {string} [stretch="linear"] - The stretch configuration for the survey or image.
  70. * @property {boolean} [reversed=false] - If true, the colormap is reversed; otherwise, it is not reversed.
  71. * @property {number} [minCut=0.0] - The minimum cut value for the color configuration. If not given, 0.0 is chosen
  72. * @property {number} [maxCut=1.0] - The maximum cut value for the color configuration. If not given, 1.0 is chosen
  73. * @property {boolean} [additive=false] - If true, additive blending is applied; otherwise, it is not applied.
  74. * @property {number} [gamma=1.0] - The gamma correction value for the color configuration.
  75. * @property {number} [saturation=0.0] - The saturation value for the color configuration.
  76. * @property {number} [brightness=0.0] - The brightness value for the color configuration.
  77. * @property {number} [contrast=0.0] - The contrast value for the color configuration.
  78. * @property {WCS} [wcs] - an object describing the WCS of the image. In case of a fits image
  79. * this property will be ignored as the WCS taken will be the one present in the fits file.
  80. * @property {string} [imgFormat] - Optional image format. Giving it will prevent the auto extension determination algorithm to be triggered. Possible values are 'jpeg', 'png' or 'fits'. tiff files are not supported. You can convert your tiff files to jpg ones by using the fantastic image magick suite.
  81. *
  82. * @example
  83. *
  84. * aladin.setOverlayImageLayer(A.image(
  85. * "https://nova.astrometry.net/image/25038473?filename=M61.jpg",
  86. * {
  87. * name: "M61",
  88. * wcs: {
  89. NAXIS: 0, // Minimal header
  90. CTYPE1: 'RA---TAN', // TAN (gnomic) projection
  91. CTYPE2: 'DEC--TAN', // TAN (gnomic) projection
  92. EQUINOX: 2000.0, // Equatorial coordinates definition (yr)
  93. LONPOLE: 180.0, // no comment
  94. LATPOLE: 0.0, // no comment
  95. CRVAL1: 185.445488837, // RA of reference point
  96. CRVAL2: 4.47896032431, // DEC of reference point
  97. CRPIX1: 588.995094299, // X reference pixel
  98. CRPIX2: 308.307905197, // Y reference pixel
  99. CUNIT1: 'deg', // X pixel scale units
  100. CUNIT2: 'deg', // Y pixel scale units
  101. CD1_1: -0.000223666022989, // Transformation matrix
  102. CD1_2: -0.000296578064584, // no comment
  103. CD2_1: -0.000296427555509, // no comment
  104. CD2_2: 0.000223774308964, // no comment
  105. NAXIS1: 1080, // Image width, in pixels.
  106. NAXIS2: 705 // Image height, in pixels.
  107. * },
  108. * successCallback: (ra, dec, fov, image) => {
  109. * aladin.gotoRaDec(ra, dec);
  110. * aladin.setFoV(fov * 5)
  111. * }
  112. * },
  113. * ));
  114. */
  115. export let Image = (function () {
  116. /**
  117. * The object describing a FITS image
  118. *
  119. * @class
  120. * @constructs Image
  121. *
  122. * @param {string} url - Mandatory unique identifier for the layer. Can be an arbitrary name
  123. * @param {ImageOptions} [options] - The option for the survey
  124. *
  125. */
  126. function Image(url, options) {
  127. // Name of the layer
  128. this.layer = null;
  129. this.added = false;
  130. // Set it to a default value
  131. this.url = url;
  132. this.id = url;
  133. this.name = (options && options.name) || this.url;
  134. this.imgFormat = options && options.imgFormat;
  135. //this.formats = [this.imgFormat];
  136. // callbacks
  137. this.successCallback = options && options.successCallback;
  138. this.errorCallback = options && options.errorCallback;
  139. this.longitudeReversed = false;
  140. this.colorCfg = new ColorCfg(options);
  141. this.options = options || {};
  142. let self = this;
  143. this.query = Promise.resolve(self);
  144. };
  145. /**
  146. * Returns the low and high cuts under the form of a 2 element array
  147. *
  148. * @memberof Image
  149. * @method
  150. *
  151. * @returns {number[]} The low and high cut values.
  152. */
  153. Image.prototype.getCuts = HiPS.prototype.getCuts;
  154. /**
  155. * Sets the opacity factor
  156. *
  157. * @memberof Image
  158. * @method
  159. * @param {number} opacity - Opacity of the survey to set. Between 0 and 1
  160. */
  161. Image.prototype.setOpacity = HiPS.prototype.setOpacity;
  162. /**
  163. * Set color options generic method for changing colormap, opacity, ...
  164. *
  165. * @memberof Image
  166. * @method
  167. * @param {Object} options
  168. * @param {number} [options.opacity=1.0] - Opacity of the survey or image (value between 0 and 1).
  169. * @param {string} [options.colormap="native"] - The colormap configuration for the survey or image.
  170. * @param {string} [options.stretch="linear"] - The stretch configuration for the survey or image.
  171. * @param {boolean} [options.reversed=false] - If true, the colormap is reversed; otherwise, it is not reversed.
  172. * @param {number} [options.minCut] - The minimum cut value for the color configuration. If not given, 0.0 for JPEG/PNG surveys, the value of the property file for FITS surveys
  173. * @param {number} [options.maxCut] - The maximum cut value for the color configuration. If not given, 1.0 for JPEG/PNG surveys, the value of the property file for FITS surveys
  174. * @param {boolean} [options.additive=false] - If true, additive blending is applied; otherwise, it is not applied.
  175. * @param {number} [options.gamma=1.0] - The gamma correction value for the color configuration.
  176. * @param {number} [options.saturation=0.0] - The saturation value for the color configuration.
  177. * @param {number} [options.brightness=0.0] - The brightness value for the color configuration.
  178. * @param {number} [options.contrast=0.0] - The contrast value for the color configuration.
  179. */
  180. Image.prototype.setOptions = HiPS.prototype.setOptions;
  181. // @api
  182. Image.prototype.setBlendingConfig = HiPS.prototype.setBlendingConfig;
  183. /**
  184. * Set the colormap of an image
  185. *
  186. * @memberof Image
  187. * @method
  188. * @param {string} [colormap="grayscale"] - The colormap label to use. See {@link https://matplotlib.org/stable/users/explain/colors/colormaps.html|here} for more info about colormaps.
  189. * Possible values are:
  190. * <br>"blues"
  191. * <br>"cividis"
  192. * <br>"cubehelix"
  193. * <br>"eosb"
  194. * <br>"grayscale"
  195. * <br>"inferno"
  196. * <br>"magma"
  197. * <br>"native"
  198. * <br>"parula"
  199. * <br>"plasma"
  200. * <br>"rainbow"
  201. * <br>"rdbu"
  202. * <br>"rdylbu"
  203. * <br>"redtemperature"
  204. * <br>"sinebow"
  205. * <br>"spectral"
  206. * <br>"summer"
  207. * <br>"viridis"
  208. * <br>"ylgnbu"
  209. * <br>"ylorbr"
  210. * <br>"red"
  211. * <br>"green"
  212. * <br>"blue"
  213. * @param {Object} [options] - Options for the colormap
  214. * @param {string} [options.stretch] - Stretching function of the colormap. Possible values are 'linear', 'asinh', 'log', 'sqrt', 'pow'. If no given, will not change it.
  215. * @param {boolean} [options.reversed=false] - Reverse the colormap axis.
  216. */
  217. Image.prototype.setColormap = HiPS.prototype.setColormap;
  218. /**
  219. * Set the cuts of the image
  220. *
  221. * @memberof Image
  222. * @method
  223. * @param {number} minCut - The low cut value.
  224. * @param {number} maxCut - The high cut value.
  225. */
  226. Image.prototype.setCuts = HiPS.prototype.setCuts;
  227. /**
  228. * Sets the gamma correction factor.
  229. *
  230. * This method updates the gamma.
  231. *
  232. * @memberof Image
  233. * @method
  234. * @param {number} gamma - The saturation value to set for the image. Between 0.1 and 10
  235. */
  236. Image.prototype.setGamma = HiPS.prototype.setGamma;
  237. /**
  238. * Sets the saturation.
  239. *
  240. * This method updates the saturation.
  241. *
  242. * @memberof Image
  243. * @method
  244. * @param {number} saturation - The saturation value. Between 0 and 1
  245. */
  246. Image.prototype.setSaturation = HiPS.prototype.setSaturation;
  247. /**
  248. * Sets the brightness.
  249. *
  250. * This method updates the brightness.
  251. *
  252. * @memberof Image
  253. * @method
  254. * @param {number} brightness - The brightness value. Between 0 and 1
  255. */
  256. Image.prototype.setBrightness = HiPS.prototype.setBrightness;
  257. /**
  258. * Sets the contrast.
  259. *
  260. * This method updates the contrast and triggers the update of metadata.
  261. *
  262. * @memberof Image
  263. * @method
  264. * @param {number} contrast - The contrast value. Between 0 and 1
  265. */
  266. Image.prototype.setContrast = HiPS.prototype.setContrast;
  267. /**
  268. * Toggle the image turning its opacity to 0 back and forth
  269. *
  270. * @memberof Image
  271. * @method
  272. */
  273. Image.prototype.toggle = HiPS.prototype.toggle;
  274. /**
  275. * Old method for setting the opacity use {@link Image#setOpacity} instead
  276. *
  277. * @memberof Image
  278. * @deprecated
  279. */
  280. Image.prototype.setAlpha = HiPS.prototype.setOpacity;
  281. Image.prototype.getColorCfg = HiPS.prototype.getColorCfg;
  282. /**
  283. * Get the opacity of the image layer
  284. *
  285. * @memberof HiPS
  286. *
  287. * @returns {number} The opacity of the layer
  288. */
  289. Image.prototype.getOpacity = HiPS.prototype.getOpacity;
  290. /**
  291. * Use {@link Image#getOpacity}
  292. *
  293. * @memberof Image
  294. * @method
  295. * @deprecated
  296. */
  297. Image.prototype.getAlpha = HiPS.prototype.getOpacity;
  298. /**
  299. * Read a specific screen pixel value
  300. *
  301. * @todo This has not yet been implemented
  302. * @memberof Image
  303. * @method
  304. * @param {number} x - x axis in screen pixels to probe
  305. * @param {number} y - y axis in screen pixels to probe
  306. * @returns {number} the value of that pixel
  307. */
  308. Image.prototype.readPixel = HiPS.prototype.readPixel;
  309. /** PRIVATE METHODS **/
  310. Image.prototype._setView = function (view) {
  311. this.view = view;
  312. this._saveInCache();
  313. };
  314. // FITS images does not mean to be used for storing planetary data
  315. Image.prototype.isPlanetaryBody = function () {
  316. return false;
  317. };
  318. // @api
  319. Image.prototype.focusOn = function () {
  320. // ensure the fits have been parsed
  321. if (this.added) {
  322. this.view.aladin.gotoRaDec(this.ra, this.dec);
  323. this.view.aladin.setFoV(this.fov);
  324. }
  325. };
  326. /* Private method view is already attached */
  327. Image.prototype._saveInCache = HiPS.prototype._saveInCache;
  328. // Private method for updating the view with the new meta
  329. Image.prototype._updateMetadata = HiPS.prototype._updateMetadata;
  330. Image.prototype._add = function (layer) {
  331. this.layer = layer;
  332. let self = this;
  333. let promise;
  334. if (this.imgFormat === 'fits') {
  335. promise = this._addFITS(layer)
  336. .catch(e => {
  337. console.error(`Image located at ${this.url} could not be parsed as fits file. Is the imgFormat specified correct?`)
  338. return Promise.reject(e)
  339. })
  340. } else if (this.imgFormat === 'jpeg' || this.imgFormat === 'png') {
  341. promise = this._addJPGOrPNG(layer)
  342. .catch(e => {
  343. console.error(`Image located at ${this.url} could not be parsed as a ${this.imgFormat} file. Is the imgFormat specified correct?`);
  344. return Promise.reject(e)
  345. })
  346. } else {
  347. // imgformat not defined we will try first supposing it is a fits file and then use the jpg heuristic
  348. promise = self._addFITS(layer)
  349. .catch(e => {
  350. return self._addJPGOrPNG(layer)
  351. .catch(e => {
  352. console.error(`Image located at ${self.url} could not be parsed as jpg/png/tif image file. Aborting...`)
  353. return Promise.reject(e);
  354. })
  355. })
  356. }
  357. promise = promise.then((imageParams) => {
  358. self.formats = [self.imgFormat];
  359. // There is at least one entry in imageParams
  360. self.added = true;
  361. self._setView(self.view);
  362. // Set the automatic computed cuts
  363. let [minCut, maxCut] = self.getCuts();
  364. minCut = minCut || imageParams.min_cut;
  365. maxCut = maxCut || imageParams.max_cut;
  366. self.setCuts(
  367. minCut,
  368. maxCut
  369. );
  370. self.ra = imageParams.centered_fov.ra;
  371. self.dec = imageParams.centered_fov.dec;
  372. self.fov = imageParams.centered_fov.fov;
  373. // Call the success callback on the first HDU image parsed
  374. if (self.successCallback) {
  375. self.successCallback(
  376. self.ra,
  377. self.dec,
  378. self.fov,
  379. self
  380. );
  381. }
  382. return self;
  383. })
  384. .catch((e) => {
  385. // This error result from a promise
  386. // If I throw it, it will not be catched because
  387. // it is run async
  388. self.view.removeImageLayer(layer);
  389. return Promise.reject(e);
  390. });
  391. return promise;
  392. };
  393. Image.prototype._addFITS = function(layer) {
  394. let self = this;
  395. return Utils.fetch({
  396. url: this.url,
  397. dataType: 'readableStream',
  398. success: (stream) => {
  399. return self.view.wasm.addImageFITS(
  400. stream,
  401. {
  402. ...self.colorCfg.get(),
  403. longitudeReversed: this.longitudeReversed,
  404. imgFormat: 'fits',
  405. },
  406. layer
  407. )
  408. },
  409. error: (e) => {
  410. // try as cors
  411. const url = Aladin.JSONP_PROXY + '?url=' + self.url;
  412. return Utils.fetch({
  413. url: url,
  414. dataType: 'readableStream',
  415. success: (stream) => {
  416. return self.view.wasm.addImageFITS(
  417. stream,
  418. {
  419. ...self.colorCfg.get(),
  420. longitudeReversed: this.longitudeReversed,
  421. imgFormat: 'fits',
  422. },
  423. layer
  424. )
  425. },
  426. });
  427. }
  428. })
  429. .then((imageParams) => {
  430. self.imgFormat = 'fits';
  431. return Promise.resolve(imageParams);
  432. })
  433. };
  434. Image.prototype._addJPGOrPNG = function(layer) {
  435. let self = this;
  436. let img = document.createElement('img');
  437. return new Promise((resolve, reject) => {
  438. img.src = this.url;
  439. img.crossOrigin = "Anonymous";
  440. img.onload = () => {
  441. const img2Blob = () => {
  442. var canvas = document.createElement("canvas");
  443. canvas.width = img.width;
  444. canvas.height = img.height;
  445. // Copy the image contents to the canvas
  446. var ctx = canvas.getContext("2d");
  447. ctx.drawImage(img, 0, 0, img.width, img.height);
  448. const imageData = ctx.getImageData(0, 0, img.width, img.height);
  449. const blob = new Blob([imageData.data]);
  450. const stream = blob.stream(1024);
  451. resolve(stream)
  452. };
  453. if (!self.options.wcs) {
  454. /* look for avm tags if no wcs is given */
  455. let avm = new AVM(img);
  456. avm.load((obj) => {
  457. // obj contains the following information:
  458. // obj.id (string) = The ID provided for the image
  459. // obj.img (object) = The image object
  460. // obj.xmp (string) = The raw XMP header
  461. // obj.wcsdata (Boolean) = If WCS have been loaded
  462. // obj.tags (object) = An array containing all the loaded tags e.g. obj.tags['Headline']
  463. // obj.wcs (object) = The wcs parsed from the image
  464. if (obj.wcsdata) {
  465. if (img.width !== obj.wcs.NAXIS1) {
  466. obj.wcs.NAXIS1 = img.width;
  467. }
  468. if (img.height !== obj.wcs.NAXIS2) {
  469. obj.wcs.NAXIS2 = img.height;
  470. }
  471. self.options.wcs = obj.wcs;
  472. img2Blob()
  473. } else {
  474. // no tags found
  475. reject('No WCS have been found in the image')
  476. return;
  477. }
  478. })
  479. } else {
  480. img2Blob()
  481. }
  482. }
  483. let proxyUsed = false;
  484. img.onerror = (e) => {
  485. // use proxy
  486. if (proxyUsed) {
  487. console.error(e);
  488. reject('Error parsing image located at: ' + self.url)
  489. return;
  490. }
  491. proxyUsed = true;
  492. img.src = Aladin.JSONP_PROXY + '?url=' + self.url;
  493. }
  494. })
  495. .then((readableStream) => {
  496. let wcs = self.options && self.options.wcs;
  497. wcs.NAXIS1 = wcs.NAXIS1 || img.width;
  498. wcs.NAXIS2 = wcs.NAXIS2 || img.height;
  499. return self.view.wasm
  500. .addImageWithWCS(
  501. readableStream,
  502. wcs,
  503. {
  504. ...self.colorCfg.get(),
  505. longitudeReversed: this.longitudeReversed,
  506. imgFormat: 'jpeg',
  507. },
  508. layer
  509. )
  510. })
  511. .then((imageParams) => {
  512. self.imgFormat = 'jpeg';
  513. return Promise.resolve(imageParams);
  514. })
  515. .finally(() => {
  516. img.remove();
  517. });
  518. };
  519. return Image;
  520. })();