Blog

A blog about astronomy and software development, mainly focused on my experiences during the development of JPARSEC.

Blog

Tag index

Recent Posts

Most recent posts.

Solar System in 3d (part 2)

In an old, previous post I showed some renderings of Mars in 3d using Java3d. That library was abandoned long ago and I mentioned I was working in a newer version using JMonkeyEngine. Indeed I did that but never spend enough time to post the results here, as I was always busy with other things. Finally, it's time to show the results I obtained.

The starting point was the jmeplanet project by Aaron Perkins, which seems to be only available now on SourceForge as spaceworld. You can also enjoy some test videos he has on Youtube. The latest code there is probably the best option now, since I took an old version of his engine. Then I prepared the Mars images for each section of the planet using the MOLA altimetry data and a high resolution texture. I encoded some RGB and bump images using the channels to avoid losing accuracy in the altimetry, so that these images can be directly loaded into the GPU and less space is needed. The code and textures used for such tasks are provided in the project link below, but obviously the original MOLA data is not included since the download is around 2 GB only for the altimetry.

As always I prefer to show results instead of describing in detail the code. In this case this is inevitable since I forgot almost everything about 3d programming (the little I learned with this project), and I developed this years ago. In addition, I simply put things together like in a puzzle, I'm not the author of the engine. But I think it can be useful and funny for others. I know a video would be better, but more time consuming, so I will put only screenshots (sorry).




 

There are some keys to control the movement (W, S, A, D; Q and E to rotate; S and X to go up and down), and in addition other keys like L and K to disable/enable the illumination and planet atmosphere. The program can also render the Earth, but using a low resolution bump map. Anyway, the global view with the atmosphere is quite nice, although not very realistic.


 

One of the few things I added to the original code (besides the real Mars textures) was the option to show an 'ocean' at a given height on the surface. The key g can be used to enable (or disable) gravity so that you will fall on the surface with the possibility of walking or even diving on the ocean. I show here two screenshots of the Chryse planitia (the region between Chryse and Vallis Marineris more exactly) in which it is evident the effects of the presence of water (erosion, sediments, lack of craters). It is also very clear the signs of the interaction of the lava with frozen water, visible as regions with many little fractures due to water vapor liberated from below the surface. The north region of Mars is about 2 km below the mean height and it is clear it was below water in ancient times. The key o will toggle the ocean at a level that emulates the approximate depth it had, which wasn't too high anyway, only a few hundred meters. This region is very interesting and some missions like Viking or Mars Pathfinder landed here. With the ocean activated it is very easy to follow its limits (the Mars coastline) and explore some others interesting places also linked to Vallis Marineris or even the Olympus Mons.



 

I had many ideas to evolve this project: identifying features, allow learning about the history of Mars, or doing the same with the Moon using the data from Clementine. Porting it to Android could be also feasible, but it seems I'm getting … old for such time demanding tasks. You are invited to download my not-so-little Mars project and play with or even evolve it. You will need of course the JMonkeyEngine library in the classpath, together with JOGL, JPARSEC, and the pngj libraries. The code is not very clean, but it works. Execute the SolarSystemTest class and enjoy!

2019/03/20 12:57 · Tomás Alonso Albi · 0 Comments

JPARSEC Sky Atlas

One of the little projects I developed some years ago was a sky atlas. It was a very first version showing the capabilites of JPARSEC to create renderings of the sky in different color modes, from the classical white background to the black one with real photographic textures of many deep sky objects. The atlas was a simplified version of the classical Sky Atlas 2000, by Wil Tirion, but updated for the latest equinox year and with less stars and objects (to print it in a more confortable A4 size). Since then the rendering options in JPARSEC has been improved, including the possibility of rendering the sky in a realistic, Stellarium-like way, but I didn't find this feature useful for this project in particular.

I didn't expect that post to become very interesting to others, but during these years some people, first from Russia, and then from Italy, showed interest and asked me to continue with this project creating the atlas for the next years and improving it. In the last two weeks I have taken some time to do some additional touches and to extend the atlas in different directions. There is now a first section showing the sky during the year with a list of astronomical events for each month, which is useful to later anallize the region in detail with the atlas itself. A first explanatory page is useful to directly go to a particular atlas chart, and in each chart I have improved the information shown and added a navigation aid. One point is that here at the Observatorio Astronómico Nacional we are working in an amateur-like equipment for outreach purposes, and this atlas could be interesting to use it in our observatory. Here we have a 30 cm and 40 cm refractors in parallel from the 1940s, an older 30 cm refractor from around 1910, and an even older meridian circle that is inside the museum building. We now have bought a Celestron C11 with a little refractor and a camera, and plan to use it mainly to show the Sun, but without sacrifying the possible use of this telescope during the night. We plan to recuperate the older telescopes also for this purpose. As a member of the Agrupación Astronómica de Madrid (the main amateur group of astronomers here) I'm also showing this atlas for the team involved in showing the sky to the group of beginners, the new people potentially joining our group. And, of course, because it is just fun to take an old project and improve it.

Probably the catalyst to this decision was the incredible Bungee Sky Atlas created by Angelo Nicolini with JPARSEC. This amazing atlas, maybe the best free atlas ever made, is extremely complete and detailed, very useful for expert observers. But one evident problem that this atlas leaves is the relatively lack of a less detailed atlas, in-between the Bungee Sky Atlas and the different atlases for beginners you can find in the web, like my favourite one the Free Beginners Star Atlas by Ed Vazhorov. Riccardo Camboni also contributed to make me work on this right now since he asked to do some improvements, for instance to remove the trajectories of planets and other bodies, which is something I may add in the future in another section of the atlas. Riccardo has also started some posts in forums to spread the existence of this atlas.


 

So in the last two weeks I have worked in the code, to clean it up enough and to publish it as GPL down below. The code is in just one laaaarge file (sorry!), since my code quality standards are limited to the library, not the projects I develop from it. There are different options in the first lines to set the color mode, language, and so on. As always I don't find interesting to extend a lot in my blog explaining the code: if you find it interesting and useful you will use it and play with it (so you will not need any help), if not, I will not lose my time to describe it in detail. Anyway, you have always the discussion section at the bottom to ask anything. There are a few todo things in Spanish at the top, so this code may improve and the 2020 version of the atlas could be even better.

You will find the new version of the atlas in Spanish and English in the Other projects section, currently only for year 2019. In case you want to create you own atlas with the code below, just remember the code is GPL and if you modify it, please publish it, and whatever atlas you may produce starting from this code must be published free of charge under the Creative Commons or similar license. You will need JPARSEC with its dependencies and the iText PDF library in the classpath to compile and execute this program. Just enjoy!

JPARSECSkyAtlas.java
/*
 * This file is part of JPARSEC library.
 * 
 * (C) Copyright 2006-2019 by T. Alonso Albi - OAN (Spain).
 *  
 * Project Info:  http://conga.oan.es/~alonso/jparsec/jparsec.html
 * 
 * JPARSEC library 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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * JPARSEC library 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */		
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
 
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.FontMapper;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfSmartCopy;
import com.itextpdf.text.pdf.PdfTemplate;
import com.itextpdf.text.pdf.PdfWriter;
 
import jparsec.astronomy.AtlasChart;
import jparsec.astronomy.Constellation;
import jparsec.astronomy.Constellation.CONSTELLATION_NAME;
import jparsec.astronomy.TelescopeElement;
import jparsec.astronomy.CoordinateSystem.COORDINATE_SYSTEM;
import jparsec.ephem.EphemerisElement;
import jparsec.ephem.EphemerisElement.COORDINATES_TYPE;
import jparsec.ephem.Functions;
import jparsec.ephem.Precession;
import jparsec.ephem.Target.TARGET;
import jparsec.ephem.stars.StarElement;
import jparsec.ephem.stars.StarEphem;
import jparsec.ephem.stars.StarEphemElement;
import jparsec.graph.chartRendering.AWTGraphics;
import jparsec.graph.chartRendering.Graphics;
import jparsec.graph.chartRendering.PlanetRenderElement;
import jparsec.graph.chartRendering.RenderPlanet;
import jparsec.graph.chartRendering.RenderSky;
import jparsec.graph.chartRendering.RenderSky.OBJECT;
import jparsec.graph.chartRendering.SkyRenderElement;
import jparsec.graph.chartRendering.Projection.PROJECTION;
import jparsec.graph.chartRendering.SkyRenderElement.COLOR_MODE;
import jparsec.graph.chartRendering.SkyRenderElement.LEYEND_POSITION;
import jparsec.graph.chartRendering.SkyRenderElement.MILKY_WAY_TEXTURE;
import jparsec.graph.chartRendering.SkyRenderElement.REALISTIC_STARS;
import jparsec.graph.chartRendering.SkyRenderElement.STAR_LABELS;
import jparsec.graph.chartRendering.SkyRenderElement.SUPERIMPOSED_LABELS;
import jparsec.graph.chartRendering.frame.SkyRendering;
import jparsec.graph.DataSet;
import jparsec.io.HTMLReport.SIZE;
import jparsec.io.HTMLReport.STYLE;
import jparsec.io.FileIO;
import jparsec.io.LATEXReport;
import jparsec.io.ReadFile;
import jparsec.io.WriteFile;
import jparsec.io.image.Picture;
import jparsec.math.Constant;
import jparsec.observer.City;
import jparsec.observer.CityElement;
import jparsec.observer.LocationElement;
import jparsec.observer.ObserverElement;
import jparsec.time.AstroDate;
import jparsec.time.DateTimeOps;
import jparsec.time.TimeElement;
import jparsec.time.TimeScale;
import jparsec.time.TimeElement.SCALE;
import jparsec.time.calendar.CalendarGenericConversion;
import jparsec.util.Configuration;
import jparsec.util.DataBase;
import jparsec.util.JPARSECException;
import jparsec.util.Logger;
import jparsec.util.Translate;
import jparsec.util.Translate.LANGUAGE;
import jparsec.vo.Feed;
import jparsec.vo.FeedMessageElement;
import jparsec.vo.GeneralQuery;
 
/**
 * A simple and practical sky atlas to show the rendering possibilities of JPARSEC.
 * Requirements: JPARSEC, iText for PDF output, Latex for atlas compilation
 * @author T. Alonso Albi - OAN (Spain)
 */
public class JPARSECSkyAtlas {
 
	private static TimeElement time;
	private static ObserverElement observer;
	private static EphemerisElement eph;
	private static SkyRenderElement sky;
	private static String suffix = "", outputPath = "";
private static int marginX = 0, marginY = 0, ppi = 300;
private static boolean showFaintNebulaInTables = false;
private static double epoch, ra0[], dec0[];
 
// TODO
// + Ultimas tres cartas: eje RA arriba
// + La version dark no muestra las estrellas mas debiles
 
// - Arreglar leyenda al usar ppi > 300
// - Cartas adicionales ? GNM, PNM, Cúmulos de galaxias de Fornax (18), Virgo (6-7, 13-14)
// 		- Mas separaciones como referencia en ejes ra/dec
// - Mejorar catalogo de variables ?
// - Quitar eventos de DST y cambiar ahi tambien los simbolos Alp, Bet etc ?
// - Trayectorias de planetas y cuerpos menores ?
 
// * Catalog of 300 - 500 double stars:
// https://www.stelledoppie.it/index2.php?metodo-cat_wds-ra=1&dato-cat_wds-ra=&metodo-cat_wds-de=1&dato-cat_wds-de=&metodo-cat_wds-raggio=6&dato-cat_wds-raggio=&metodo-cat_wds-coord_2000=1&dato-cat_wds-coord_2000=&metodo-cat_wds-discov_num=1&dato-cat_wds-discov_num=&metodo-cat_wds-comp=1&dato-cat_wds-comp=&metodo-cat_wds-name=9&dato-cat_wds-name=&metodo-cat_wds-date_first=1&dato-cat_wds-date_first=&metodo-cat_wds-date_last=1&dato-cat_wds-date_last=&metodo-cat_wds-mag_pri=5&dato-cat_wds-mag_pri=7&metodo-cat_wds-mag_sec=5&dato-cat_wds-mag_sec=8.5&metodo-cat_wds-calc_delta_mag=5&dato-cat_wds-calc_delta_mag=&metodo-cat_wds-calc_sep=3&dato-cat_wds-calc_sep=10&metodo-cat_wds-spectr=11&dato-cat_wds-spectr=&metodo-calc_wds_other-Bayer=9&dato-calc_wds_other-Bayer=&metodo-calc_wds_other-Flamsteed=1&dato-calc_wds_other-Flamsteed=&metodo-cat_wds-cst=1&dato-cat_wds-cst=&metodo-calc_wds_other-ADS=1&dato-calc_wds_other-ADS=&metodo-calc_wds_other-HD=1&dato-calc_wds_other-HD=&metodo-calc_wds_other-HR=1&dato-calc_wds_other-HR=&metodo-calc_wds_other-HIP=1&dato-calc_wds_other-HIP=&metodo-calc_wds_other-SAO=1&dato-calc_wds_other-SAO=&metodo-calc_wds_other-Tycho2=1&dato-calc_wds_other-Tycho2=&metodo-calc_wds_other-Gaia=9&dato-calc_wds_other-Gaia=&metodo-cat_wds-dm_number=1&dato-cat_wds-dm_number=&metodo-cat_wds-obs=3&dato-cat_wds-obs=&metodo-cat_wds-notes=9&dato-cat_wds-notes=&metodo-cat_wds_notes-notes=9&dato-cat_wds_notes-notes=&metodo-cat_wds-reports=3&dato-cat_wds-reports=&metodo-cat_wds-filtro_visuale=1&metodo-cat_wds-filtro_strumento=1&metodo-cat_wds-filtro_coord=1&metodo-cat_wds-filtro_orbita=1&metodo-cat_wds-filtro_nome=1&metodo-cat_wds-filtro_principale=1&metodo-cat_wds-filtro_fisica=1&metodo-cat_wds-filtro_incerta=1&metodo-cat_wds-calc_tot_comp=1&dato-cat_wds-calc_tot_comp=&menu=21&section=2&azione=cerca_nel_database&limite=&righe=50&orderby=&type=3&set_filtri=S&gocerca=Search+the+database
 
private enum DOUBLE_STAR_CATALOG {
	JPARSEC,
	ST500,
	ST1000
}
 
/**
 * Main program.
 * @param args Not used.
 */
public static void main(String args[]) {
	try {
		// Main atlas options
		Translate.setDefaultLanguage(Translate.LANGUAGE.ENGLISH);
		int year = 2019;
		boolean showAstroEvents = true;
		int monthStepForSeasonalSky = 1;
		DOUBLE_STAR_CATALOG dstar = DOUBLE_STAR_CATALOG.ST500;
		showFaintNebulaInTables = true;
 
		boolean hideSeasonalSky = false;
		boolean whiteAtlas = true;
		boolean useBeamerTitles = true;
		boolean showLeyendInChart = false;
		boolean useUT = !(Translate.getDefaultLanguage() == LANGUAGE.SPANISH), useTiny = true;
		outputPath = "/home/alonso/";
		//outputPath = "/home/alonso/documentos/latex/todos/atlasServidorEfem/"+year+"/";
		int width = 210, height = 297; // A4
		ppi = 300;
		String cityName = "Madrid";
 
		// Prepare stuff
		ArrayList<String> listObj = new ArrayList<String>();
		ArrayList<String> listDouble = new ArrayList<String>();
		ArrayList<String> listVar = new ArrayList<String>();
		ArrayList<String> listEvent = new ArrayList<String>();
 
		if (monthStepForSeasonalSky > 1) showAstroEvents = false;
		COLOR_MODE cm = COLOR_MODE.BLACK_BACKGROUND;
		if (whiteAtlas) cm = COLOR_MODE.PRINT_MODE;
		eph = new EphemerisElement(TARGET.NOT_A_PLANET, EphemerisElement.COORDINATES_TYPE.APPARENT,
				EphemerisElement.EQUINOX_OF_DATE, EphemerisElement.TOPOCENTRIC, EphemerisElement.REDUCTION_METHOD.IAU_2006,
				EphemerisElement.FRAME.DYNAMICAL_EQUINOX_J2000);
		eph.preferPrecisionInEphemerides = false;
		eph.correctForEOP = eph.correctForPolarMotion = false;
		CityElement city = City.findCity(cityName);
		observer = ObserverElement.parseCity(city);
		width = (int) (0.5+width * ppi / 25.4) - marginX * 2;
		height = (int) (0.5+height * ppi / 25.4) - marginY * 2;
		PlanetRenderElement render = new PlanetRenderElement(false, true, true, false, false);
		TelescopeElement telescope = TelescopeElement.BINOCULARS_7x50;
		telescope.ocular.focalLength = 370f;
		RenderPlanet.ALLOW_SPLINE_RESIZING = false;
		RenderPlanet.FORCE_HIGHT_QUALITY = true;
		RenderPlanet.MAXIMUM_TEXTURE_QUALITY_FACTOR = 2;
		render.highQuality = true;
		if (!whiteAtlas) Configuration.MAX_CACHE_SIZE = 1;
 
		// Prepare sky rendering object for the seasonal sky
		sky = new SkyRenderElement(COORDINATE_SYSTEM.HORIZONTAL,
				PROJECTION.STEREOGRAPHICAL, 0, 0.0, width, height, render, telescope);
		sky.setColorMode(cm);
		sky.drawDeepSkyObjectSymbolMinimumSize = 7;
		sky.drawConstellationNamesType = Constellation.CONSTELLATION_NAME.LATIN;
		if (Translate.getDefaultLanguage() == Translate.LANGUAGE.SPANISH)
			sky.drawConstellationNamesType = Constellation.CONSTELLATION_NAME.SPANISH;
		sky.drawStarsLabels = SkyRenderElement.STAR_LABELS.ONLY_PROPER_NAME;
		if (Translate.getDefaultLanguage() == Translate.LANGUAGE.SPANISH)
			sky.drawStarsLabels = SkyRenderElement.STAR_LABELS.ONLY_PROPER_NAME_SPANISH;
		sky.drawObjectsLimitingMagnitude = 6.5f;
		sky.drawDeepSkyObjectsAllMessierAndCaldwell = false;
		sky.drawPlanetsMoonSun = true;
		sky.drawSpaceProbes = false;
		sky.drawStarsGreekSymbolsOnlyIfHasProperName = false;
		sky.drawTransNeptunianObjects = false;
		sky.drawStarsLimitingMagnitude = 6.5f;
		sky.drawClever = false;
		if (height >= 15000) sky.drawStarsLimitingMagnitude = 7.5f;
		sky.drawStarsLabelsLimitingMagnitude = 4;
		sky.drawArtificialSatellites = false;
		sky.drawArtificialSatellitesOnlyThese = "ISS,HST";
		sky.drawAsteroids = true;
		sky.drawComets = true;
		sky.drawSuperNovaAndNovaEvents = false;
		sky.drawSunSpots = false;
		sky.drawDeepSkyObjectsLabels = true;
		sky.drawMilkyWayContoursWithTextures = MILKY_WAY_TEXTURE.OPTICAL;
		sky.drawStarsPositionAngleInDoubles = true;
 
		sky.drawSkyCorrectingLocalHorizon = true;
		sky.drawConstellationNamesFont = Graphics.FONT.DIALOG_ITALIC_28;
		sky.drawStarsNamesFont = Graphics.FONT.DIALOG_PLAIN_16;
		sky.drawPlanetsNamesFont = Graphics.FONT.DIALOG_PLAIN_24;
		sky.drawCoordinateGridFont = Graphics.FONT.DIALOG_PLAIN_23;
		sky.drawDeepSkyObjectsNamesFont = Graphics.FONT.SANS_SERIF_ITALIC_12;
		sky.drawMinorObjectsNamesFont = Graphics.FONT.SANS_SERIF_PLAIN_12;
 
		sky.drawConstellationNamesFont = Graphics.FONT.getDerivedFont(sky.drawConstellationNamesFont, (sky.drawConstellationNamesFont.getSize() * ppi) / 300);
		sky.drawStarsNamesFont = Graphics.FONT.getDerivedFont(sky.drawStarsNamesFont, (sky.drawStarsNamesFont.getSize() * ppi) / 300);
		sky.drawPlanetsNamesFont = Graphics.FONT.getDerivedFont(sky.drawPlanetsNamesFont, (sky.drawPlanetsNamesFont.getSize() * ppi) / 300);
		sky.drawCoordinateGridFont = Graphics.FONT.getDerivedFont(sky.drawCoordinateGridFont, (sky.drawCoordinateGridFont.getSize() * ppi) / 300);
		sky.drawDeepSkyObjectsNamesFont = Graphics.FONT.getDerivedFont(sky.drawDeepSkyObjectsNamesFont, (sky.drawDeepSkyObjectsNamesFont.getSize() * ppi) / 300);
		sky.drawMinorObjectsNamesFont = Graphics.FONT.getDerivedFont(sky.drawMinorObjectsNamesFont, (sky.drawMinorObjectsNamesFont.getSize() * ppi) / 300);
 
		sky.drawConstellationLimits = false;
		sky.drawFastLabels = SUPERIMPOSED_LABELS.AVOID_SUPERIMPOSING_VERY_ACCURATE;
		sky.drawFastLabelsInWideFields = false;
		sky.drawStarsRealistic = REALISTIC_STARS.STARRED;
		sky.drawLeyend = LEYEND_POSITION.TOP;
		sky.drawCoordinateGridCardinalPoints = false;
 
		sky.drawSkyBelowHorizon = false;
		sky.drawStarsGreekSymbols = true;
		sky.centralLongitude = 180.0 * Constant.DEG_TO_RAD;
		sky.centralLatitude = Math.PI * 0.5; //observer.latitude;
		if (height <= 3000) sky.planetRender.highQuality = true;
 
		if (sky.getColorMode() == COLOR_MODE.WHITE_BACKGROUND
				|| sky.getColorMode() == COLOR_MODE.PRINT_MODE) {
			sky.drawMilkyWayContoursWithTextures = MILKY_WAY_TEXTURE.NO_TEXTURE;
			sky.drawDeepSkyObjectsTextures = false;
			sky.drawStarsRealistic = REALISTIC_STARS.NONE_CUTE;
			sky.fillMilkyWay = true;
			sky.drawStarsColor = 255<<24 | 0<<16 | 0<<8 | 0;
		}
 
		// Generate a layout similar to Sky Atlas 2000 by Wil Tirion
		ra0 = new double[] {
				4, 12, 20, 2, 6, 10, 14, 18, 22, 2, 5, 8, 11, 14, 17, 20, 23, 2, 6, 10, 14, 18, 22, 4, 12, 20
		};
		dec0 = new double[] {
				70, 70, 70, 35, 35, 35, 35, 35, 35, 0, 0, 0, 0, 0, 0, 0, 0, -35, -35, -35, -35, -35, -35, -70, -70, -70
		};
 
		marginX = marginY = 0;
		LATEXReport latex = new LATEXReport();
		boolean spa = Translate.getDefaultLanguage() == LANGUAGE.SPANISH;
		String title = spa ? "Atlas Celeste JPARSEC y calendario de eventos para "+year : "JPARSEC Sky Atlas and calendar events for "+year, country = spa ? "España" : "Spain";
		if (!showAstroEvents)
			title = spa ? "Atlas Celeste JPARSEC "+year : "JPARSEC Sky Atlas for "+year;
		String institute = "Observatorio Astronómico Nacional, Madrid ("+country+")", date = institute, 
				author = "Tomás Alonso Albi";
		epoch = Constant.J2000 + (year - 2000) * Constant.JULIAN_DAYS_PER_CENTURY * 0.01;
		institute = null;
		String margincm = "0.1";
		if (!hideSeasonalSky) {
			latex.writeHeaderForPresentationUsingBeamer(null, title, author, institute, date, new String[] {"colortbl"}, true, false, true, margincm, margincm);
			String code = latex.getCode();
			code = DataSet.replaceAll(code, "\\usepackage{hyperref}", "\\usepackage{hyperref}"+FileIO.getLineSeparator()+"\\geometry{paperwidth=297mm,paperheight=210mm}", true); // 128x96 is default
			code = DataSet.replaceAll(code, "\\end{document}", "", true);
			latex.setCode(code);
			latex.writeRawText("\\definecolor{LightCyan}{rgb}{0.88,1,1}"+FileIO.getLineSeparator());
			latex.beginFrame(true, true);
			latex.writeRawText(latex.writeImage("50%", null, "center", null, null, "/home/alonso/oan/otros/cover.png"));
			latex.endFrame();
 
			latex.setLineSpacing(1.0);
 
			if (useBeamerTitles) {
				latex.beginFrame(true, false);
				String sep = FileIO.getLineSeparator();
				String s = spa ? 
					"Este sencillo atlas celeste está dirigido a aficionados de nivel medio que no necesitan un atlas básico ni tampoco "+sep+
					"excesivamente detallado. Está diseñado para ser impreso a doble cara en tamaño A4, y utilizado en modo apaisado. La primera parte muestra el cielo a lo largo de "+sep+
					"los meses junto con una lista detallada de eventos astronómicos. Los límites de magnitud son 6.5 para "+sep+
					"estrellas y objetos de cielo profundo. La segunda sección muestra el atlas celeste en sí, con "+sep+
					"estrellas hasta la magnitud 8.25 y objetos hasta la magnitud 11 (algunos más débiles aparecerán si están listados por Messier o Caldwell). Para cada carta se muestra una tabla con "+sep+
					"los objetos de cielo profundo visibles en ella, ordenados por magnitud. También aparecen las principales "+sep+
					"estrellas dobles (tomadas de https://www.stelledoppie.it, con primaria/secundaria más brillante que magnitud 7/8.5 y con una separación mínima de 10\") y variables. Las coordenadas para el atlas son astrométricas referidas a la época Juliana correspondiente "+sep+"al año del atlas. Algunos eventos u objetos no aparecen en las tablas cuando hay en ellas demasiadas entradas. "+sep+
					"El número total de estrellas del atlas es de unas 60000, con unos 1100 objetos de cielo profundo (850 en las tablas), 450 dobles y 75 variables. El número total de eventos astronómicos es de unos 1100."+sep+
					"Este atlas es gratuito y ha sido desarrollado por Tomás Alonso, a partir de "+sep+
					"la librería astronómica JPARSEC (http://conga.oan.es/\\%7Ealonso/doku.php?id=jparsec), la cual se distribuye como software libre bajo licencia GPL. Este documento se distribuye bajo los términos de la licencia Creative Commons."
					: 
					"This simple sky atlas is focused on amateur astronomers that don't need a basic atlas neither a too much detailed one. "+sep+
					"It is designed to be printed in long side duplex mode and A4 size, and handled in landscape orientation. The first part shows the sky for each month, "+sep+
					"including a detailed list of the astronomical events for each month. Limiting magnitudes are 6.5 for "+sep+
					"stars and deep sky objects. The second section shows the sky atlas itself, with stars up to "+sep+
					"magnitude 8.25 and deep sky objects brighter than magnitude 11 (some fainter ones will appear if they are in Messier or Caldwell lists). For each chart there is a table showing "+sep+
					"the deep sky objects visibles on it, sorted by magnitude. Additional tables show the main double stars (from https://www.stelledoppie.it, primary/secondary brighter than magnitude 7/8.5 and with a minimum separation of 10\") and "+sep+
					"variable stars on that chart. Coordinates for the atlas are astrometric, referred to the Julian epoch of the atlas year. "+sep+"Some events or objects could not appear in the tables when there are too much entries. "+sep+
					"The total number of stars in the atlas is around 60000, with 1100 deep sky objects (850 in the tables), 450 double stars and 75 variable stars. The total number of astronomical events is around 1100."+sep+
					"This atlas is free and has been developed by Tomás Alonso, using the "+sep+
					"JPARSEC astronomical package (http://conga.oan.es/\\%7Ealonso/doku.php?id=jparsec), which is distributed as free software under GPL license. This document is distributed under the terms of the Creative Commons license.";
				latex.writeRawText("\\begin{center}"+sep+"\\Normal "+s+sep+"\\end{center}"+sep);
 
				latex.writeTableHeader(null);
				latex.setTextStyle(STYLE.BOLD);
				latex.writeRowInTable(
						spa ? new String[] {"Carta", "Ascensión recta central (h)", "Declinación central (\u00b0)", "Constelaciones"} : 
							new String[] {"Chart", "Central right ascension (h)", "Central declination (\u00b0)", "Constellations"}, null, null, null);
				latex.setTextStyle(STYLE.PLAIN);
				latex.writeHorizontalLine();
				for (int ii=0; ii<ra0.length; ii++) {
					LocationElement loc0 = new LocationElement(ra0[ii]/Constant.RAD_TO_HOUR, dec0[ii]*Constant.DEG_TO_RAD, 1);
					String cons = getConstellations(loc0, 5);
					String col[] = new String[] {""+(ii+1), ""+ra0[ii], ""+dec0[ii], cons};
					latex.writeRowInTable(col, null, null, null);					
				}
				latex.endTable();					
				latex.endFrame();
				latex.beginFrame(true, false);
				s = spa ? "El cielo estacional" : "Seasonal Sky";
				latex.writeRawText("\\begin{center}"+sep+"\\Huge "+s+sep+"\\end{center}"+sep);
				latex.endFrame();
				latex.writeRawText("\\setbeamertemplate{headline}{}"+sep);
				latex.writeRawText("\\setbeamertemplate{footline}{}"+sep);
			}
		} else {
			title = "JPARSEC Sky atlas for "+year;
			if (Translate.getDefaultLanguage() == LANGUAGE.SPANISH) title = "Atlas celeste de JPARSEC para "+year;
			latex.writeHeaderForPresentationUsingBeamer(null, title, author, institute, date, null, true, false, true, "0.2", "0.2");
			String code = latex.getCode();
			code = DataSet.replaceAll(code, "\\usepackage{hyperref}", "\\geometry{paperwidth=256mm,paperheight=192mm}", true);
			code = DataSet.replaceAll(code, "\\end{document}", "", true);
			latex.setCode(code);
			latex.beginFrame(true, true);
			latex.writeRawText(latex.writeImage("50%", null, "center", null, null, "/home/alonso/oan/otros/cover.png"));
			latex.endFrame();				
		}
		author = institute;
		latex.setTextSize(SIZE.VERY_SMALL);
		latex.setAvoidFormatting(latex.getAvoidFormatting()+"_$");
		latex.writeRawText("\\newcommand\\omicron{o}");
 
		String label = null, align = "left", caption = null;
		int maxLines = 56;
		// Show seasonal sky, possibly with events
		if (!hideSeasonalSky) {
			caption = null;
			sky.trajectory = null;
			sky.coordinateSystem = COORDINATE_SYSTEM.HORIZONTAL;
			sky.centralLatitude = 0;
			sky.centralLongitude = Math.PI;
			latex.setTextSize(SIZE.NORMAL);
			latex.beginSection(spa ? "Firmamento y lista detallada de eventos astronómicos mes a mes" : "Sky and events month by month");
			for (int month = 1; month <= 12; month = month + monthStepForSeasonalSky) {
				String month1 = CalendarGenericConversion.getMonthName(month, CalendarGenericConversion.CALENDAR.GREGORIAN);
				latex.beginSubSection(spa ? "Firmamento en "+month1.toLowerCase() : "Sky on "+month1.toLowerCase());
				for (int day = 15; day < 20; day = day + 14) {					
					AstroDate astro = new AstroDate(year, month, day, 0, 0, 0);
					caption = (spa ? "Firmamento el "+astro.toStringDate(true).toLowerCase()+" h" : "Sky on "+astro.toStringDate(true)+" h");
					time = new TimeElement(astro, SCALE.LOCAL_TIME);
					double jd = TimeScale.getJD(time, observer, eph, TimeElement.SCALE.BARYCENTRIC_DYNAMICAL_TIME);
					time = new TimeElement(jd, TimeElement.SCALE.BARYCENTRIC_DYNAMICAL_TIME);
 
					suffix = "seasonal_"+astro.getYear()+"_"+astro.getMonth();
					createSeasonalSkyChart();
					String src = suffix+".pdf";
 
					// Show astronomical events for the month
					if (showAstroEvents) {
						latex.beginFrame(true, false);
						latex.writeRawText("\\begin{scriptsize}"+FileIO.getLineSeparator());
						//latex.writeRawText("\\vspace{0.2cm}"+FileIO.getLineSeparator());
						latex.beginColumns();
						latex.beginColumn("0.5");						
						latex.writeTableHeader(null);	
						latex.setTextStyle(STYLE.BOLD);
						if (useUT) {
							latex.writeRowInTable(
								spa ? new String[] {"Fecha (TU)", "Descripción del evento"} : 
									new String[] {"Date (UT)", "Event description"}, null, null, null);
						} else {
							latex.writeRowInTable(
									spa ? new String[] {"Fecha (hora local)", "Descripción del evento"} : 
										new String[] {"Date (Spain local time)", "Event description"}, null, null, null);								
						}
						latex.setTextStyle(STYLE.PLAIN);
						latex.writeHorizontalLine();
						String data[] = getEvents(year, month, cityName.equals("Tenerife"));
						int middle = data.length/2;
						if (middle > maxLines) middle = maxLines;
						for (int ii=0; ii<middle; ii++) {
							String info[] = DataSet.toStringArray(data[ii], "@");
							if (info.length == 1) info = new String[] {info[0], ""};
							int cd = info[1].indexOf("Coordenadas de");
							if (cd > 0) info[1] = info[1].substring(0, cd).trim();
							int par = info[1].indexOf("Paralaje");
							if (par > 0) {
								String after = info[1].substring(par);
								after = after.substring(after.indexOf(",")+1).trim();
								info[1] = info[1].substring(0, par) + after;
							}
							info[1] = DataSet.replaceAll(info[1], "Distancia", "d", true);
							info[1] = DataSet.replaceAll(info[1], " TL", "", true);
							info[1] = DataSet.replaceAll(info[1], ""+year+"-"+DateTimeOps.twoDigits(month)+"-", "", true);
							String cityName2 = cityName;
							if (cityName.equals("Tenerife")) cityName2 = "Santa Cruz de Tenerife";
							info[0] = DataSet.replaceAll(info[0], "Máximo no visible desde "+cityName2, "No visible", true);
							info[1] = DataSet.replaceAll(info[1], "Máximo no visible desde "+cityName2, "No visible", true);
							info[0] = DataSet.replaceAll(info[0], "Salida/puesta del Sol sobre el cráter", "0º alt Sol en el", true);
 
							info[1] = DataSet.replaceAll(info[1], "Distance", "d", true);
							info[1] = DataSet.replaceAll(info[1], " LT", "", true);
							info[0] = DataSet.replaceAll(info[0], "Maximum not visible from "+cityName2, "Not visible", true);
							info[1] = DataSet.replaceAll(info[1], "Maximum not visible from "+cityName2, "Not visible", true);
							info[0] = DataSet.replaceAll(info[0], "Rise/set of Sun from crater", "0º alt Sun on", true);
							int nocoord = info[0].indexOf(". Coordinates of");
							if (nocoord >= 0) info[0] = info[0].substring(0, nocoord);
							nocoord = info[1].indexOf(". Coordinates of");
							if (nocoord >= 0) info[1] = info[1].substring(0, nocoord);
 
							par = info[0].lastIndexOf("(");
							if (par > 0) info[0] = info[0].substring(0, par).trim();
							int par1 = info[1].indexOf(", saros"), par2 = info[1].indexOf(", semidura");
							if (par2 < 0) {
								par2 = info[1].indexOf("semi-dura");
								if (par2 > 0) {
									info[1] = info[1].substring(0, par2);
									info[1] = info[1].substring(0, info[1].lastIndexOf(","))+")";
									par1 = par2 = -1;
								}
							}
							if (par2 > 0) {
								String after = info[1].substring(par2);
								int par3 = after.indexOf(".");
								if (par3 > 0) {
									after = after.substring(par3);
								} else {
									after = ")";
								}
								info[1] = info[1].substring(0, par2) + after;
							} else {
								if (par1 > 0) {
									String after = info[1].substring(par1);
									int par3 = after.indexOf(".");
									if (par3 > 0) {
										after = after.substring(par3);
									} else {
										after = ")";
									}
									info[1] = info[1].substring(0, par1) + after;
								}
							}
							if (info[1].endsWith(".")) info[1] = info[1].substring(0, info[1].length()-1);
							if (info[1].indexOf("No visible:") >= 0 || 
									info[1].indexOf("Not visible:") >= 0) {
								info[1] = DataSet.replaceAll(info[1], 
										spa ? "No visible:" : "Not visible:", "", true).trim();
								info[0] += spa ? ". No visible" : ". Not visible";
							}
							int ll = info[1].indexOf("Posible lluvia de meteoros");
							if (ll < 0) ll = info[1].indexOf("Possible meteor shower");
							if (ll > 0) {
								info[0] = info[1].substring(ll);
								info[1] = info[1].substring(0, ll).trim();
								if (info[1].endsWith(".")) info[1] = info[1].substring(0, info[1].length()-1);
							}
							ll = info[1].indexOf("(");
							if (ll > 0) {
								info[0] += " "+info[1].substring(ll);
								info[1] = info[1].substring(0, ll).trim();
							}
							if (useUT) info[1] = toUT(info[1], year, month);
							if (ii % 2 == 1) latex.writeRawText("\\rowcolor{LightCyan}"+FileIO.getLineSeparator());
 
							String i0 = info[0].toLowerCase();
							if (i0.indexOf("lunar eclipse") >= 0 || i0.indexOf("solar eclipse") >= 0 || i0.indexOf("new moon") >= 0 ||
								i0.indexOf("eclipse lunar") >= 0 || i0.indexOf("eclipse solar") >= 0 || i0.indexOf("luna nueva") >= 0 || 
								i0.indexOf("gemínidas") >= 0 || i0.indexOf("cuadrántidas") >= 0 || i0.indexOf("perseidas") >= 0 || 
								i0.indexOf("geminids") >= 0 || i0.indexOf("quadrantids") >= 0 || i0.indexOf("perseids") >= 0 
								)
									latex.setTextStyle(STYLE.BOLD);		
							info[1] = DataSet.replaceOne(info[1], " ", ", ", 1);
							if (info[1].indexOf("->") > 0) {
								String s1 = info[1].substring(0, info[1].indexOf("->") + 2);
								String sr = info[1].substring(info[1].indexOf("->") + 2).trim();
								sr = DataSet.replaceOne(sr, " ", ", ", 1);
								String srcoma = sr.substring(0, sr.indexOf(",")+1);
								if (s1.startsWith(srcoma)) sr = sr.substring(sr.indexOf(",")+1).trim();
								info[1] = s1 + " " + sr;
							}
							info[1] = DataSet.replaceOne(info[1], "->", "$\\rightarrow$", 1);
							info[0] = DataSet.replaceAll(info[0], ". No visible", "", true);
							info[0] = DataSet.replaceAll(info[0], ". Not visible", "", true);
							info[0] = DataSet.capitalize(info[0], false);
							latex.writeRowInTable(new String[] {info[1], info[0]}, null, null, null);	
							listEvent.add(info[1]+info[0]);
							latex.setTextStyle(STYLE.PLAIN);
						}
						latex.endTable();
						latex.endColumn();
						latex.beginColumn("0.5");												
						latex.writeTableHeader(null);
						latex.setTextStyle(STYLE.BOLD);
						if (useUT) {
							latex.writeRowInTable(
								spa ? new String[] {"Fecha (TU)", "Descripción del evento"} : 
									new String[] {"Date (UT)", "Event description"}, null, null, null);
						} else {
							latex.writeRowInTable(
									spa ? new String[] {"Fecha (hora local)", "Descripción del evento"} : 
										new String[] {"Date (Spain local time)", "Event description"}, null, null, null);								
						}
						latex.setTextStyle(STYLE.PLAIN);
						latex.writeHorizontalLine();
						middle = data.length/2;
						if (middle > maxLines) middle = data.length-maxLines;
						for (int ii=middle; ii<data.length; ii++) {
							String info[] = DataSet.toStringArray(data[ii], "@");
							if (info.length == 1) info = new String[] {info[0], ""};
							int cd = info[1].indexOf("Coordenadas de");
							if (cd > 0) info[1] = info[1].substring(0, cd).trim();
							int par = info[1].indexOf("Paralaje");
							if (par > 0) {
								String after = info[1].substring(par);
								after = after.substring(after.indexOf(",")+1).trim();
								info[1] = info[1].substring(0, par) + after;
							}
							info[1] = DataSet.replaceAll(info[1], "Distancia", "d", true);
							info[1] = DataSet.replaceAll(info[1], " TL", "", true);
							info[1] = DataSet.replaceAll(info[1], ""+year+"-"+DateTimeOps.twoDigits(month)+"-", "", true);
							String cityName2 = cityName;
							if (cityName.equals("Tenerife")) cityName2 = "Santa Cruz de Tenerife";
							info[0] = DataSet.replaceAll(info[0], "Máximo no visible desde "+cityName2, "No visible", true);
							info[1] = DataSet.replaceAll(info[1], "Máximo no visible desde "+cityName2, "No visible", true);
							info[0] = DataSet.replaceAll(info[0], "Salida/puesta del Sol sobre el cráter", "0º alt Sol en el", true);
 
							info[1] = DataSet.replaceAll(info[1], "Distance", "d", true);
							info[1] = DataSet.replaceAll(info[1], " LT", "", true);
							info[0] = DataSet.replaceAll(info[0], "Maximum not visible from "+cityName2, "Not visible", true);
							info[1] = DataSet.replaceAll(info[1], "Maximum not visible from "+cityName2, "Not visible", true);
							info[0] = DataSet.replaceAll(info[0], "Rise/set of Sun from crater", "0º alt Sun on", true);
							int nocoord = info[0].indexOf(". Coordinates of");
							if (nocoord >= 0) info[0] = info[0].substring(0, nocoord);
							nocoord = info[1].indexOf(". Coordinates of");
							if (nocoord >= 0) info[1] = info[1].substring(0, nocoord);
 
							par = info[0].lastIndexOf("(");
							if (par > 0) info[0] = info[0].substring(0, par).trim();
							int par1 = info[1].indexOf(", saros"), par2 = info[1].indexOf(", semidura");
							if (par2 < 0) {
								par2 = info[1].indexOf("semi-dura");
								if (par2 > 0) {
									info[1] = info[1].substring(0, par2);
									info[1] = info[1].substring(0, info[1].lastIndexOf(","))+")";
									par1 = par2 = -1;
								}
							}
							if (par2 > 0) {
								String after = info[1].substring(par2);
								int par3 = after.indexOf(".");
								if (par3 > 0) {
									after = after.substring(par3);
								} else {
									after = ")";
								}
								info[1] = info[1].substring(0, par2) + after;
							} else {
								if (par1 > 0) {
									String after = info[1].substring(par1);
									int par3 = after.indexOf(".");
									if (par3 > 0) {
										after = after.substring(par3);
									} else {
										after = ")";
									}
									info[1] = info[1].substring(0, par1) + after;
								}
							}
							if (info[1].endsWith(".")) info[1] = info[1].substring(0, info[1].length()-1);
							if (info[1].indexOf("No visible:") >= 0 || 
									info[1].indexOf("Not visible:") >= 0) {
								info[1] = DataSet.replaceAll(info[1], 
										spa ? "No visible:" : "Not visible:", "", true).trim();
								info[0] += spa ? ". No visible" : ". Not visible";
							}
							int ll = info[1].indexOf("Posible lluvia de meteoros");
							if (ll < 0) ll = info[1].indexOf("Possible meteor shower");
							if (ll > 0) {
								info[0] = info[1].substring(ll);
								info[1] = info[1].substring(0, ll).trim();
								if (info[1].endsWith(".")) info[1] = info[1].substring(0, info[1].length()-1);
							}
							ll = info[1].indexOf("(");
							if (ll > 0) {
								info[0] += " "+info[1].substring(ll);
								info[1] = info[1].substring(0, ll).trim();
							}
							if (useUT) info[1] = toUT(info[1], year, month);
							if (ii % 2 == 1) latex.writeRawText("\\rowcolor{LightCyan}"+FileIO.getLineSeparator());
 
							String i0 = info[0].toLowerCase();
							if (i0.indexOf("lunar eclipse") >= 0 || i0.indexOf("solar eclipse") >= 0 || i0.indexOf("new moon") >= 0 ||
									i0.indexOf("eclipse lunar") >= 0 || i0.indexOf("eclipse solar") >= 0 || i0.indexOf("luna nueva") >= 0 || 
									i0.indexOf("gemínidas") >= 0 || i0.indexOf("cuadrántidas") >= 0 || i0.indexOf("perseidas") >= 0 || 
									i0.indexOf("geminids") >= 0 || i0.indexOf("quadrantids") >= 0 || i0.indexOf("perseids") >= 0 
									)
										latex.setTextStyle(STYLE.BOLD);									
							info[1] = DataSet.replaceOne(info[1], " ", ", ", 1);
							if (info[1].indexOf("->") > 0) {
								String s1 = info[1].substring(0, info[1].indexOf("->") + 2);
								String sr = info[1].substring(info[1].indexOf("->") + 2).trim();
								sr = DataSet.replaceOne(sr, " ", ", ", 1);
								String srcoma = sr.substring(0, sr.indexOf(",")+1);
								if (s1.startsWith(srcoma)) sr = sr.substring(sr.indexOf(",")+1).trim();
								info[1] = s1 + " " + sr;
							}
							info[1] = DataSet.replaceOne(info[1], "->", "$\\rightarrow$", 1);
							info[0] = DataSet.replaceAll(info[0], ". No visible", "", true);
							info[0] = DataSet.replaceAll(info[0], ". Not visible", "", true);
 
							info[0] = DataSet.capitalize(info[0], false);
							latex.writeRowInTable(new String[] {info[1], info[0]}, null, null, null);					
							listEvent.add(info[1]+info[0]);
							latex.setTextStyle(STYLE.PLAIN);
						}
						latex.endTable();
						latex.endColumn();
						latex.endColumns();
						latex.writeRawText(FileIO.getLineSeparator()+"\\end{scriptsize}"+FileIO.getLineSeparator());
 
						//latex.writeRawText("\\vspace{-0.6cm}"+FileIO.getLineSeparator());
						//latex.setTextStyle(STYLE.BOLD);
						if (spa) {
							latex.writeParagraph("Eventos astronómicos para el mes de "+astro.getMonthName().toLowerCase());						
						} else {
							latex.writeParagraph("Astronomical events for "+astro.getMonthName());						
						} 
						//latex.setTextStyle(STYLE.PLAIN);
						latex.endFrame();
					}
 
					// Show seasonal sky
					if (useBeamerTitles) {
						latex.beginFrame(caption);
						caption = null;
					} else {
						latex.beginFrame(true, false);					
						latex.writeRawText("\\vspace{0.8cm}" + FileIO.getLineSeparator());
					}
					latex.writeImageWithCaption("100%", null, align, src, caption, label);
					latex.endFrame();
				}
			}
		}
 
		// Adapt sky and ephemeris objects for the astrometric sky atlas section
		eph.ephemType = COORDINATES_TYPE.ASTROMETRIC;
		eph.equinox = (new AstroDate(year, 1, 1, 12, 0, 0)).jd();
		eph.isTopocentric = false;
		sky.setColorMode(COLOR_MODE.BLACK_BACKGROUND);
		if (whiteAtlas) sky.setColorMode(COLOR_MODE.PRINT_MODE);
		sky.coordinateSystem = COORDINATE_SYSTEM.EQUATORIAL;
		sky.drawMilkyWayContoursWithTextures = MILKY_WAY_TEXTURE.OPTICAL;
		sky.drawDeepSkyObjectsTextures = !whiteAtlas;
		if (sky.getColorMode() == COLOR_MODE.WHITE_BACKGROUND
				|| sky.getColorMode() == COLOR_MODE.PRINT_MODE) {
			sky.drawMilkyWayContoursWithTextures = MILKY_WAY_TEXTURE.NO_TEXTURE;
			sky.drawDeepSkyObjectsTextures = false;
			sky.drawStarsRealistic = REALISTIC_STARS.NONE_CUTE;
			sky.fillMilkyWay = true;
			sky.drawStarsColor = 255<<24 | 0<<16 | 0<<8 | 0;
		}
 
		if (!hideSeasonalSky) {
			latex.beginSection(spa ? "Atlas celeste para "+year : "Sky atlas for "+year);
		} else {
			if (Translate.getDefaultLanguage() == LANGUAGE.SPANISH) {
				latex.beginSection("Atlas Celeste de JPARSEC para "+year);			
			} else {
				latex.beginSection("JPARSEC Sky Atlas for "+year);								
			}
		}
		if (useBeamerTitles) {
			latex.beginFrame(true, false);
			latex.endFrame();
			latex.beginFrame(true, false);
			String sep = FileIO.getLineSeparator();
			String s = spa ? "Atlas Celeste, época J"+year : "Sky Atlas, J"+year+" epoch";
			latex.writeRawText("\\begin{center}"+sep+"\\Huge "+s+sep+"\\end{center}"+sep);
			latex.endFrame();
		}
 
		SCALE wscale = SCALE.UNIVERSAL_TIME_UTC;
 
		AstroDate astro = new AstroDate(year, 1, 1);
		time = new TimeElement(astro, SCALE.BARYCENTRIC_DYNAMICAL_TIME);
		double jdUT = TimeScale.getJD(time, observer, eph, wscale);
		time = new TimeElement(jdUT, wscale);
 
		double init = astro.jd(), dur = 365;
		double end = init + dur;
		init = TimeScale.getJD(new TimeElement(init,  wscale), observer, eph, SCALE.BARYCENTRIC_DYNAMICAL_TIME);
		end = TimeScale.getJD(new TimeElement(end,  wscale), observer, eph, SCALE.BARYCENTRIC_DYNAMICAL_TIME);
 
		sky.drawConstellationNamesFont = Graphics.FONT.DIALOG_ITALIC_32;
		sky.drawStarsNamesFont = Graphics.FONT.DIALOG_PLAIN_20;
		sky.drawPlanetsNamesFont = Graphics.FONT.DIALOG_PLAIN_28;
		sky.drawCoordinateGridFont = Graphics.FONT.DIALOG_PLAIN_27;
		sky.drawDeepSkyObjectsNamesFont = Graphics.FONT.SANS_SERIF_ITALIC_16;
		sky.drawMinorObjectsNamesFont = Graphics.FONT.SANS_SERIF_PLAIN_16;
 
		sky.drawConstellationNamesFont = Graphics.FONT.getDerivedFont(sky.drawConstellationNamesFont, (sky.drawConstellationNamesFont.getSize() * ppi) / 300);
		sky.drawStarsNamesFont = Graphics.FONT.getDerivedFont(sky.drawStarsNamesFont, (sky.drawStarsNamesFont.getSize() * ppi) / 300);
		sky.drawPlanetsNamesFont = Graphics.FONT.getDerivedFont(sky.drawPlanetsNamesFont, (sky.drawPlanetsNamesFont.getSize() * ppi) / 300);
		sky.drawCoordinateGridFont = Graphics.FONT.getDerivedFont(sky.drawCoordinateGridFont, (sky.drawCoordinateGridFont.getSize() * ppi) / 300);
		sky.drawDeepSkyObjectsNamesFont = Graphics.FONT.getDerivedFont(sky.drawDeepSkyObjectsNamesFont, (sky.drawDeepSkyObjectsNamesFont.getSize() * ppi) / 300);
		sky.drawMinorObjectsNamesFont = Graphics.FONT.getDerivedFont(sky.drawMinorObjectsNamesFont, (sky.drawMinorObjectsNamesFont.getSize() * ppi) / 300);
 
		sky.drawStarsLimitingMagnitude = 8.25f;
		sky.drawObjectsLimitingMagnitude = 11f;
		sky.drawSkyBelowHorizon = true;
		sky.drawSkyCorrectingLocalHorizon = false;
		sky.drawConstellationLimits = true;
		sky.drawComets = sky.drawAsteroids = false;
		//sky.drawStarsPositionAngleInDoubles = false;
		sky.drawPlanetsMoonSun = false;
		sky.drawDeepSkyObjectsAllMessierAndCaldwell = true;
		sky.drawMeteorShowers = true;
		sky.drawMeteorShowersOnlyActive = false;
		if (Translate.getDefaultLanguage() == LANGUAGE.SPANISH) {
			sky.drawConstellationNamesType = Constellation.CONSTELLATION_NAME.SPANISH;
		} else {
			sky.drawConstellationNamesType = Constellation.CONSTELLATION_NAME.LATIN;				
		}
		sky.drawClever = false;
		sky.width = (2990 * ppi) / 300;
		sky.height = (2000 * ppi) / 300;
		//sky.drawLeyend = LEYEND_POSITION.RIGHT;
		if (!showLeyendInChart) sky.drawLeyend = LEYEND_POSITION.NO_LEYEND;
		FileIO.deleteFile(outputPath + "leyend.pdf");
		double field = (35 * sky.width) / sky.height;
		sky.telescope.ocular.focalLength = TelescopeElement.getOcularFocalLengthForCertainField(field * Constant.DEG_TO_RAD, sky.telescope);
		latex.setTextSize(SIZE.NORMAL);
 
		for (int i=0; i<ra0.length; i++) {
			String constel = getConstellations(
					new LocationElement(ra0[i] / Constant.RAD_TO_HOUR, dec0[i] * Constant.DEG_TO_RAD, 1), 
					4);
			String co[] = DataSet.toStringArray(constel, ",", false);
			if (Translate.getDefaultLanguage() == LANGUAGE.SPANISH && sky.drawConstellationNamesType == CONSTELLATION_NAME.SPANISH) {
				for (int j=0;j<co.length;j++) {
					co[j] = Constellation.getConstellation(co[j], Constellation.CONSTELLATION_NAME.SPANISH); 						
				}
			} 
			constel = DataSet.toString(co, ", ");
			String caption2 = null, Jxxxx = "";
			Jxxxx = " (J"+year+".0)";
			if (hideSeasonalSky) {
				String add = "";
				if (!whiteAtlas) add = ", dark edition";
				if (Translate.getDefaultLanguage() == LANGUAGE.SPANISH) {
					if (!whiteAtlas) add = ", edición en negro";
					caption = "Atlas Celeste de JPARSEC para "+year+""+add+". Carta "+(i+1)+", 40º alrededor de "+ra0[i]+"h, "+dec0[i]+"º ("+constel+"). (c) Tomás Alonso, "+astro.getYear();
					caption2 = "Carta "+(i+1)+", 30º alrededor de "+ra0[i]+"h, "+dec0[i]+"º ("+constel+")";
				} else {
					caption = "JPARSEC Sky Atlas for "+year+""+add+". Chart "+(i+1)+", 40º around "+ra0[i]+"h, "+dec0[i]+"º ("+constel+"). (c) Tomás Alonso, "+astro.getYear();					
					caption2 = "Chart "+(i+1)+", 30º around "+ra0[i]+"h, "+dec0[i]+"º ("+constel+")";
				}					
			} else {
				if (Translate.getDefaultLanguage() == LANGUAGE.SPANISH) {
					caption = "Carta "+(i+1)+", 30º alrededor de "+ra0[i]+"h, "+dec0[i]+"º ("+constel+")";
					caption2 = caption;
				} else {
					caption = "Chart "+(i+1)+", 30º around "+ra0[i]+"h, "+dec0[i]+"º ("+constel+")";					
					caption2 = caption;
				}
			}
			if (Translate.getDefaultLanguage() == LANGUAGE.SPANISH) {
				latex.beginSubSection(DataSet.replaceAll(caption2, "º", "g", true));
			} else {
				latex.beginSubSection(DataSet.replaceAll(caption2, "º", "d", true));					
			}
			sky.centralLongitude = ra0[i] / Constant.RAD_TO_HOUR;
			sky.centralLatitude = dec0[i] * Constant.DEG_TO_RAD;
			suffix = "chart"+i;
			String data[] = createAtlasChart();
 
			// Show list of objects sorted by magnitude				
			int nmax = data.length, maxLines2 = maxLines - 6;
			boolean tiny = false;
			latex.beginFrame(true, false);
			boolean twoColumn = true;
			if (twoColumn) {
				latex.beginColumns();
				latex.beginColumn("0.58");
			}
			if (nmax > maxLines2 && useTiny && (i == 25 || i == 24 || i == 21 || i == 18 || i == 11 || i == 0)) {
				latex.writeRawText("\\begin{tiny}"+FileIO.getLineSeparator());
				maxLines2 = 70;
				tiny = true;
			} else {
				latex.writeRawText("\\begin{scriptsize}"+FileIO.getLineSeparator());
			}
			latex.writeTableHeader(null);
			latex.setTextStyle(STYLE.BOLD);
			String sizes = spa ? "Diámetro angular (')" : "Angular size (')"; // Translate.translate(308)
			String sra = spa ? "AR" : "RA", sdec = "DEC";
			latex.writeRowInTable(new String[] {Translate.translate(506), 
					sra+Jxxxx, sdec, 
					//Translate.translate(21)+Jxxxx, Translate.translate(22), 
					Translate.translate(157), Translate.translate(486), sizes}, null, null, null);
			latex.setTextStyle(STYLE.PLAIN);
			latex.writeHorizontalLine();
 
			if (nmax > maxLines2) nmax = maxLines2;
 
			for (int ii=0; ii<nmax; ii++) {
				String datai[] = DataSet.toStringArray(data[ii], ",", false);
				datai[1] = Functions.formatRA(Double.parseDouble(datai[1]), 1);
				datai[2] = Functions.formatDEC(Double.parseDouble(datai[2]), 0);
				datai[2] = DataSet.replaceOne(datai[2], "\"", "$\"$", 1);
				if (!datai[2].startsWith("-")) datai[2] = "+"+datai[2];
				if (datai[3].equals("")) datai[3] = "100";
				datai[3] = Functions.formatValue(Double.parseDouble(datai[3]), 1);
				if (datai[3].equals("100.0")) datai[3] = "";
				datai[4] = DataSet.capitalize(datai[4], false);
				//if (datai[5].length() > 11) {
					String f1 = FileIO.addSpacesAfterAString(FileIO.getField(1, datai[5], "x", false), 5);
					String f2 = FileIO.addSpacesAfterAString(FileIO.getField(2, datai[5], "x", false), 5);
					double d1 = Double.parseDouble(f1);
					String unit = "";
					if (d1 < 0.75) {
						if (d1*2*60 < 0.75) {
							if (d1 == 0) {
								f1 = "";
								f2 = "";
								unit = "";
							} else {
								d1 *= 60;
								unit = "\"";
								f1 = ""+Functions.formatValue(d1*2*60, 1);
								if (!f2.equals("")) f2 = ""+Functions.formatValue(Double.parseDouble(f2)*2*60*60, 1);							
							}
						} else {
							f1 = ""+Functions.formatValue(d1*2*60, 1);
							if (!f2.equals("")) f2 = ""+Functions.formatValue(Double.parseDouble(f2)*2*60, 1);							
						}
					} else {
						unit = "\u00b0";
						f1 = ""+Functions.formatValue(d1*2, 2);
						if (!f2.equals("")) f2 = ""+Functions.formatValue(Double.parseDouble(f2)*2, 2);
					}
					datai[5] = f1.substring(0, Math.min(5, f1.length())).trim()+unit;
					if (!f2.equals("") && !f2.equals(f1)) datai[5] += " x "+f2.substring(0, Math.min(5, f2.length())).trim() + unit;
				//} else {
				//	datai[5] = DataSet.replaceAll(datai[5], " ", "", true);
				//	String unit = "\u00b0";
				//	datai[5] = DataSet.replaceOne(datai[5], "x", unit+" x ", 1) + unit;
				//}
				datai[5] = DataSet.replaceAll(datai[5], ".000", ".0", true);
				datai[5] = DataSet.replaceAll(datai[5], ".00", ".0", true);
				if (ii % 2 == 1) latex.writeRawText("\\rowcolor{LightCyan}"+FileIO.getLineSeparator());
 
				String ff = datai[0].trim().substring(0, 1);
				if (DataSet.isDoubleStrictCheck(ff)) {
					datai[0] = "NGC" + datai[0];
				} else {
					if (datai[0].trim().startsWith("I.")) {
						datai[0] = "IC" + datai[0].trim().substring(2).trim();
					}
				}
 
				latex.writeRowInTable(datai, null, null, null);		
 
				String entry = DataSet.toString(datai, ";");
				if (!listObj.contains(entry)) listObj.add(entry);
			}
			latex.endTable();
			if (tiny) {
				latex.writeRawText("\\end{tiny}"+FileIO.getLineSeparator());
			} else {
				latex.writeRawText("\\end{scriptsize}"+FileIO.getLineSeparator());
			}
 
			if (twoColumn) {
				latex.endColumn();
				latex.beginColumn("0.41");
				latex.writeRawText("\\begin{scriptsize}"+FileIO.getLineSeparator());
 
				double skyFieldRadius = sky.telescope.getField() * 0.5 * 1.2;
				LocationElement skyLoc = new LocationElement(sky.centralLongitude, sky.centralLatitude, 1);
				StarEphem.READ_STARS_BEYOND_MAG_6_5 = true;
				StarEphem.resetStars();
				nmax = 25;
				int ntotal = 0, ntotalMax = 31;
				double whratio = ((double) sky.height) / sky.width;
 
				if (spa) {
					latex.writeParagraph("Principales estrellas dobles");						
				} else {
					latex.writeParagraph("Main double stars");						
				} 
				latex.writeTableHeader(null);
				latex.setTextStyle(STYLE.BOLD);
				if (dstar == DOUBLE_STAR_CATALOG.JPARSEC) {
					latex.writeRowInTable(new String[] {Translate.translate(506), 
						//Translate.translate(21)+Jxxxx, Translate.translate(22), 
						sra+Jxxxx, sdec, 
						Translate.translate(157)}, null, null, null);
				} else {
					String sep = spa ? "Sep (\") / AP (\u00b0)" : "Sep (\") / PA (\u00b0)";
					latex.writeRowInTable(new String[] {Translate.translate(506), 
							//Translate.translate(21)+Jxxxx, Translate.translate(22), 
							sra+Jxxxx, sdec, 
							Translate.translate(157), sep}, null, null, null);						
				}
				latex.setTextStyle(STYLE.PLAIN);
				latex.writeHorizontalLine();
				int n = 0;
				SkyRenderElement sky = JPARSECSkyAtlas.sky.clone();
				int yMargin = 0;
				SkyRendering skyRender = new SkyRendering(time, observer, eph, sky, "Sky render", yMargin);
				skyRender.getRenderSkyObject().setYCenterOffset(1);
				if (dstar == DOUBLE_STAR_CATALOG.JPARSEC) {
					for (int ii=0; ii<StarEphem.MAIN_DOUBLE_STARS.length; ii++) {
						int index = StarEphem.MAIN_DOUBLE_STARS[ii];
						StarEphemElement se = StarEphem.starEphemeris(time, observer, eph, StarEphem.getStarElement(index), false);
						if (se == null) continue;
						if (LocationElement.getApproximateAngularDistance(se.getEquatorialLocation(), skyLoc) < skyFieldRadius) {
							if (se.declination < skyLoc.getLatitude() - skyFieldRadius * whratio) continue;
							if (se.declination > skyLoc.getLatitude() + skyFieldRadius * whratio) continue;
							if (skyRender.getRenderSkyObject().getSkyPosition(se.getEquatorialLocation(), true, true, false) == null) continue;
							String name = se.name;
							if (name.indexOf("(") > 0) {
								name = name.substring(name.indexOf("("));
								name = name.substring(0, name.indexOf(")")+1);
							} else {
								name = "Sky2000Master "+name;
							}
							String proper = StarEphem.getStarProperNameFromCatalogName(StarEphem.getStarName(index));
							if (proper != null && proper.indexOf("/") > 0) proper = proper.substring(0, proper.indexOf("/")).trim();
							if (proper != null) name = proper + " " + name;
							if (name.startsWith("(")) name = name.substring(1, name.indexOf(")"));
							name = replaceGreek(name);
							String datai[] = new String[] {name.trim(), Functions.formatRA(se.rightAscension, 1), Functions.formatDEC(se.declination, 0), Functions.formatValue(se.magnitude, 1)};
 
							String entry = DataSet.toString(datai, ";");
							if (!listDouble.contains(entry)) listDouble.add(entry);
 
							latex.writeRowInTable(datai, null, null, null);	
							n++;
							ntotal ++;
							if (n >= nmax) break;
						}
					}
				} else {
					String path = "/home/alonso/documentos/latex/todos/atlasServidorEfem/2019/";
					path += (dstar == DOUBLE_STAR_CATALOG.ST1000) ? "1000.csv" : "500.csv";
					String sdata[] = DataSet.arrayListToStringArray(ReadFile.readAnyExternalFile(path));
					String table[] = new String[0]; 
					double mag[] = new double[0];
					for (int ii=4;ii<sdata.length;ii++) {
						String f[] = DataSet.toStringArray(sdata[ii], ";", false);
						String ra = f[4].substring(0, 9).trim();
						String dec = f[4].substring(9).trim();
						LocationElement loc = new LocationElement(Functions.parseRightAscension(ra), Functions.parseDeclination(dec), 1);
						if (LocationElement.getApproximateAngularDistance(loc, skyLoc) < skyFieldRadius) {
							if (loc.getLatitude() < skyLoc.getLatitude() - skyFieldRadius * whratio) continue;
							if (loc.getLatitude() > skyLoc.getLatitude() + skyFieldRadius * whratio) continue;
							if (skyRender.getRenderSkyObject().getSkyPosition(loc, true, true, false) == null) continue;
 
							String name = f[5];
							if (!f[1].equals("")) name += " ("+f[1]+")";
							loc = LocationElement.parseRectangularCoordinates(Precession.precessFromJ2000(epoch, loc.getRectangularCoordinates(), eph));
							ra = Functions.formatRA(loc.getLongitude(), 1);
							dec = Functions.formatDEC(loc.getLatitude(), 0);
 
							name = replaceGreek(name);
							String sep = f[9] + " / " + f[8];
							sep = DataSet.replaceAll(sep, "999.90", "-", true);
							sep = DataSet.replaceAll(sep, ".00", ".0", true);
							name = DataSet.replaceAll(name, "Double Double", spa ? "Cuádruple" : "Quadruple", true);
							String entry = DataSet.toString(new String[] {name, ra, dec, f[10]+" / "+f[11], sep}, "@");
							table = DataSet.addStringArray(table, entry);
							mag = DataSet.addDoubleArray(mag, new double[] {Double.parseDouble(f[10])});
							//latex.writeRowInTable(new String[] {name, ra, dec, f[10]+" / "+f[11]}, null, null, null);	
						}
					}
					table = DataSet.sortInCrescent(table, mag);
					for (int ii=0; ii<table.length;ii++) {
 
						if (!listDouble.contains(table[ii])) listDouble.add(table[ii]);
 
						latex.writeRowInTable(DataSet.toStringArray(table[ii], "@", false), null, null, null);								
						n++;
						ntotal ++;
						if (n >= nmax) break;
					}
				}
				latex.endTable();				
 
				if (i != 17 && i < 23) { // Hardcoded charts with no famous variable stars
					if (spa) {
						latex.writeParagraph("Principales estrellas variables");						
					} else {
						latex.writeParagraph("Main variable stars");						
					} 
 
					latex.writeTableHeader(null);
					latex.setTextStyle(STYLE.BOLD);
					latex.writeRowInTable(new String[] {Translate.translate(506), 
							//Translate.translate(21)+Jxxxx, Translate.translate(22), 
							sra+Jxxxx, sdec, 
							Translate.translate(157)}, null, null, null);
					latex.setTextStyle(STYLE.PLAIN);
					latex.writeHorizontalLine();
					n = 0;
					for (int ii=0; ii<StarEphem.MAIN_VARIABLE_STARS.length; ii++) {
						int index = StarEphem.MAIN_VARIABLE_STARS[ii];
						StarEphemElement se = StarEphem.starEphemeris(time, observer, eph, StarEphem.getStarElement(index), false);
						if (se == null) continue;
						if (LocationElement.getApproximateAngularDistance(se.getEquatorialLocation(), skyLoc) < skyFieldRadius) {
							if (se.declination < skyLoc.getLatitude() - skyFieldRadius * whratio) continue;
							if (se.declination > skyLoc.getLatitude() + skyFieldRadius * whratio) continue;
							if (skyRender.getRenderSkyObject().getSkyPosition(se.getEquatorialLocation(), true, true, false) == null) continue;
							String name = se.name;
							if (name.indexOf("(") > 0) {
								name = name.substring(name.indexOf("("));
								name = name.substring(0, name.indexOf(")")+1);
							} else {
								name = "Sky2000Master "+name;
							}
							String proper = StarEphem.getStarProperNameFromCatalogName(StarEphem.getStarName(index));
							if (proper != null && proper.indexOf("/") > 0) proper = proper.substring(0, proper.indexOf("/")).trim();
							if (proper != null) name = proper + " " + name;
							if (name.startsWith("(")) name = name.substring(1, name.indexOf(")"));
							name = replaceGreek(name);
							String datai[] = new String[] {name, Functions.formatRA(se.rightAscension, 1), Functions.formatDEC(se.declination, 0), Functions.formatValue(se.magnitude, 1)};
							latex.writeRowInTable(datai, null, null, null);			
 
							String entry = DataSet.toString(datai, ";");
							if (!listVar.contains(entry)) listVar.add(entry);
 
							n++;
							ntotal ++;
							if (n >= nmax || ntotal >= ntotalMax) break;
						}
					}
					latex.endTable();	
				}
 
				if (spa) {
					latex.writeParagraph("Mapa de navegación");						
				} else {
					latex.writeParagraph("Navigation map");						
				} 
				exportNavigationMapToPDFUsingiText(outputPath + "nav"+i+".pdf", i+1);
				latex.writeRawText("\\vspace{-0.3cm}"+FileIO.getLineSeparator());
				latex.writeImageWithCaption("100%", null, align, "nav"+i+".pdf", null, label);
 
				latex.writeRawText("\\end{scriptsize}"+FileIO.getLineSeparator());
				latex.endColumn();
				latex.endColumns();
			}
 
			latex.writeRawText("\\vspace{-0.2cm}"+FileIO.getLineSeparator());
			//latex.setTextStyle(STYLE.BOLD);
			if (spa) {
				latex.writeParagraph("Principales objetos visibles en la carta "+(i+1));						
			} else {
				latex.writeParagraph("Main objects visible on chart "+(i+1));						
			} 
			//latex.setTextStyle(STYLE.PLAIN);
 
			if (!showLeyendInChart) {
				createLeyendChart();
				String cap = spa ? "Leyenda del gráfico" : "Chart leyend";
				latex.writeImageWithCaption("100%", null, align, "leyend.pdf", null, label);
				latex.writeRawText("\\vspace{-0.9cm}"+FileIO.getLineSeparator());
				latex.writeParagraph(cap);						
			}
 
			latex.endFrame();
 
			// Show the sky chart
			if (useBeamerTitles) {
				latex.beginFrame(caption);					
				latex.writeRawText("\\vspace{-0.2cm}"+FileIO.getLineSeparator());
				caption = null;
			} else {
				latex.beginFrame(true, false);					
				latex.writeRawText("\\vspace{0.8cm}"+FileIO.getLineSeparator());
			}				
			latex.writeImageWithCaption("100%", null, align, suffix+".pdf", caption, label);
			latex.endFrame();			
		}
 
		latex.endBody();
		latex.endDocument();
 
		String add = spa ? "_spa" : "";
		String out = outputPath + "JPARSECSkyAtlas" + year + add + ".tex";
		String code = latex.getCode();
		code = DataSet.replaceAll(code, "\\smallskip\n\\caption{Chart", "\\vspace{-1.1cm}\n\\caption{Chart", true);
		code = DataSet.replaceAll(code, "\\usepackage[usenames]{color}", "", true);
		code = DataSet.replaceAll(code, "ht=2.5ex", "ht=4.5ex", true);
		code = DataSet.replaceAll(code, "\"\\%)", "\")", true);
		WriteFile.writeAnyExternalFile(out, code);
 
		// Create pdf file.
		LATEXReport.compileLatexToPDF(out);
 
		// Show file
		//ApplicationLauncher.launchDefaultViewer(out.substring(0, out.lastIndexOf("."))+".pdf");
 
		StarEphem.READ_STARS_BEYOND_MAG_6_5 = true;
		StarEphem.resetStars();
		int n = 60000, dn = 1000;
		while(true) {
			StarElement star = StarEphem.getStarElement(n);
			if (star.magnitude == 8.25) break;
			if (star.magnitude > 8.25) {
				if (dn > 0) dn = -dn / 4;
			} else {
				if (dn < 0) dn = -dn / 4;
			}
			n += dn;
		}
		int nEvent = DataSet.getDifferentElements(DataSet.arrayListToStringArray(listEvent)).length;
		int nObj = DataSet.getDifferentElements(DataSet.arrayListToStringArray(listObj)).length;
		int nDouble = DataSet.getDifferentElements(DataSet.arrayListToStringArray(listDouble)).length;
		int nVar = DataSet.getDifferentElements(DataSet.arrayListToStringArray(listVar)).length;
 
		System.out.println("# stars:    "+n);
		System.out.println("# events:    "+nEvent);
		System.out.println("# objects:   "+nObj+" (en tablas, unos 1100 en el atlas)");
		System.out.println("# doubles:   "+nDouble);
		System.out.println("# variables: "+nVar);
	} catch (Exception ve) {
		Logger.log(Logger.LEVEL.ERROR, JPARSECException.getTrace(ve.getStackTrace()));
		ve.printStackTrace();
	}
}
 
 
private static String[] createAtlasChart()
{
	try {
		RenderPlanet.dateChanged();
		SkyRenderElement sky = JPARSECSkyAtlas.sky.clone();
		int yMargin = 0;
		SkyRendering skyRender = new SkyRendering(time, observer, eph, sky, "Sky render", yMargin);
		skyRender.getRenderSkyObject().setYCenterOffset(1);
 
    	String data[] = exportToPDFUsingiText(skyRender, new Dimension(sky.width, sky.height), outputPath + suffix+".pdf", true);
		sky = null;
		DataBase.clearEntireDatabase();
		return data;
	} catch (Exception ve)
	{
		Logger.log(Logger.LEVEL.ERROR, JPARSECException.getTrace(ve.getStackTrace()));
		ve.printStackTrace();
	}
	return null;
}
 
private static void createLeyendChart()
{
	try {
		if (FileIO.exists(outputPath + "leyend.pdf")) return;
		RenderPlanet.dateChanged();
		SkyRenderElement sky = JPARSECSkyAtlas.sky.clone();
		sky.drawLeyend = LEYEND_POSITION.TOP;
		sky.height = 395;
		//sky.drawCoordinateGrid = false;
		sky.drawCoordinateGridLabels = false;
		sky.drawCoordinateGridEcliptic = false;
		sky.drawSkyBelowHorizon = false;
		sky.drawCoordinateGridCardinalPoints = false;
		sky.drawStars = false;
		//sky.drawConstellationContours = CONSTELLATION_CONTOUR.NONE;
		//sky.drawConstellationLimits = false;
		//sky.drawDeepSkyObjects = false;
		sky.drawDeepSkyObjectsLabels = sky.drawMagnitudeLabels = sky.drawPlanetsLabels = false;
		sky.drawStarsLabels = STAR_LABELS.NONE;
 
		sky.drawConstellationNamesFont = Graphics.FONT.getDerivedFont(sky.drawConstellationNamesFont, (sky.drawConstellationNamesFont.getSize() * ppi) / 300);
		sky.drawStarsNamesFont = Graphics.FONT.getDerivedFont(sky.drawStarsNamesFont, (sky.drawStarsNamesFont.getSize() * ppi) / 300);
		sky.drawPlanetsNamesFont = Graphics.FONT.getDerivedFont(sky.drawPlanetsNamesFont, (sky.drawPlanetsNamesFont.getSize() * ppi) / 300);
		sky.drawCoordinateGridFont = Graphics.FONT.getDerivedFont(sky.drawCoordinateGridFont, (sky.drawCoordinateGridFont.getSize() * ppi) / 300);
		sky.drawDeepSkyObjectsNamesFont = Graphics.FONT.getDerivedFont(sky.drawDeepSkyObjectsNamesFont, (sky.drawDeepSkyObjectsNamesFont.getSize() * ppi) / 300);
		sky.drawMinorObjectsNamesFont = Graphics.FONT.getDerivedFont(sky.drawMinorObjectsNamesFont, (sky.drawMinorObjectsNamesFont.getSize() * ppi) / 300);
 
		int yMargin = 0;
		SkyRendering skyRender = new SkyRendering(time, observer, eph, sky, "Sky render", yMargin);
 
		exportToPDFUsingiText(skyRender, new Dimension(sky.width, 95), outputPath + "leyend.pdf", false);
		sky = null;
		DataBase.clearEntireDatabase();
	} catch (Exception ve)
	{
		Logger.log(Logger.LEVEL.ERROR, JPARSECException.getTrace(ve.getStackTrace()));
		ve.printStackTrace();
	}
}
 
/**
 * For unit testing only.
 */
private static int[] createSeasonalSkyChart()
{
	try
	{
		SkyRenderElement sky = JPARSECSkyAtlas.sky.clone();
		TelescopeElement tel = TelescopeElement.OBJECTIVE_50mm_f1_4;
		tel.ocular.focalLength = 150f;
		sky.telescope = tel;
		sky.planetRender.telescope = tel;
		sky.drawSkyBelowHorizon = true;
		sky.height = sky.width/2 + (int) (30*(ppi/25.4));
		sky.centralLatitude = 0;
		int yMargin = sky.height/2-150;
		SkyRendering skyRender = new SkyRendering(time, observer, eph, sky, "Sky render", yMargin);
		exportToPDFUsingiText(skyRender, new Dimension(sky.width, sky.height), outputPath + suffix+".pdf", false);
		int w = sky.width, h = sky.height;
		sky = null;
		DataBase.clearEntireDatabase();
		return new int[] {w, h};
	} catch (Exception ve) {
		Logger.log(Logger.LEVEL.ERROR, JPARSECException.getTrace(ve.getStackTrace()));
		ve.printStackTrace();
	}
	return null;
}
 
private static String[] exportToPDFUsingiText(SkyRendering skyRender, Dimension size, String plotFile, boolean returnObjects) {
	int factor = 1;
	size = new Dimension((int)(0.5 + (size.width+marginX*2)/factor), (int)(0.5 + (size.height+marginY*2)/factor));
	try {
		com.itextpdf.text.Rectangle pageSize = new com.itextpdf.text.Rectangle(0, 0, size.width, size.height);
		Document document = new Document(pageSize);
		PdfWriter writer = PdfSmartCopy.getInstance(document, new FileOutputStream(plotFile));
		writer.setCompressionLevel(9);
		writer.setFullCompression();
		document.open();
 
		// Create a custom font mapper that forces the use of the fonts we want
        FontMapper mapper = new FontMapper() {
        	String javaFonts[] = new String[] {"Dialog", "SansSerif", "Serif"};
            public BaseFont awtToPdf(Font font) {
                try {
                	int which = DataSet.getIndex(javaFonts, font.getName());
                	if (which < 0) {
                		which = 1;
                		System.out.println("Font "+font.getName()+" not found. Using "+javaFonts[which]+" instead.");
                	}
                    if (which == 2) return BaseFont.createFont(BaseFont.SYMBOL, BaseFont.CP1250, BaseFont.EMBEDDED);
 
                	String name = BaseFont.HELVETICA;
                	if (font.isBold()) name = BaseFont.HELVETICA_BOLD;
                	if (font.isItalic()) {
                		name = BaseFont.HELVETICA_OBLIQUE;
                		if (font.isBold()) name = BaseFont.HELVETICA_BOLDOBLIQUE;
                	}
                    return BaseFont.createFont(name, BaseFont.CP1252, BaseFont.EMBEDDED);
                } catch (DocumentException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }
            public Font pdfToAwt(BaseFont font, int size) { return null; }
        };
 
        PdfContentByte canvas = writer.getDirectContent();
        PdfTemplate template = canvas.createTemplate(size.width, size.height);
        canvas.addTemplate(template, 0, 0);
 
        String out[] = null;
		// Vector graphics
        Graphics2D g2 = template.createGraphics(size.width, size.height, mapper, false, 1f); //canvas.createGraphics(pageSize.getWidth(), pageSize.getHeight(), false, 1f);
        g2.setColor(Color.WHITE);
        g2.fillRect(0, 0, size.width, size.height);
        g2.scale(1.0/(double)factor, 1.0/(double)factor);
        g2.translate(marginX, marginY);
		skyRender.paintChart(g2);
		g2.setColor(new Color(sky.background));
		g2.setClip(0, 0, sky.width*2, sky.height*2);
		/*
		if (sky.coordinateSystem == COORDINATE_SYSTEM.HORIZONTAL) {
			g2.fillRect(0, size.height-44, 50, 50);					
		} else {
			g2.fillRect(0, size.height-52, 50, 60);
		}
		g2.fillRect(0, size.height-2, size.width, 2);
		*/
		if (size.height < 100) {
			g2.setColor(Color.WHITE);
			g2.fillRect(0, 92, size.width, size.height);
		}
		g2.dispose();
 
		Object o =  DataBase.getData("minorObjects", skyRender.getRenderSkyObject().getDataBaseThreadID(), true);
		if (o != null && returnObjects) {
			if (showFaintNebulaInTables) {
				SkyRenderElement sky = skyRender.getRenderSkyObject().render.clone();
				sky.drawObjectsLimitingMagnitude = 110;
				sky.drawStarsLimitingMagnitude = 4;
				skyRender.getRenderSkyObject().setSkyRenderElement(sky);
				skyRender.getRenderSkyObject().resetLeyend(true);
				skyRender.createBufferedImage();
			}
 
			ArrayList<Object> objects = new ArrayList<Object>(Arrays.asList((Object[]) o)); 
			ArrayList<String> data = new ArrayList<String>();
			String sep = ",";
			String objt[] = new String[8];
			int types2Int[] = new int[] {819, 40, 959, 960, 1297, 961, 953, 954, 955, 956, 957, 958};
			for (int type = 0; type <= 7; type ++) {
				objt[type] = Translate.translate(types2Int[type]).toLowerCase();
			}
			for (int i=0; i<objects.size(); i++)  
			{
				Object[] obj = (Object[]) objects.get(i);
				RenderSky.OBJECT objID = (OBJECT) obj[0];
				if (objID == OBJECT.DEEPSKY) {
					LocationElement loc = (LocationElement) obj[2];
					Object more[] = (Object[]) obj[5];
					float mag = (Float) obj[3];
					float ss[] = (float[]) more[1];
					String ssize = ss[0]+"x"+ss[1];
					String d[] = new String[] {
							(String) obj[4], ""+loc.getLongitude(), ""+loc.getLatitude(), 
							""+mag, objt[Integer.parseInt((String)more[0])], ssize, (String) more[2]
					}; 
					if (mag >= 50 && ss[0]*60 > 1) {
						d[3] = "";
						data.add(0, d[0]+sep+d[1]+sep+d[2]+sep+d[3]+sep+d[4]+sep+d[5]);							
					} else {
						data.add(d[0]+sep+d[1]+sep+d[2]+sep+d[3]+sep+d[4]+sep+d[5]);
					}
				}
			}
			out = DataSet.arrayListToStringArray(data);
		}
 
		document.close();
		return out;
	} catch (Exception exc) {
		exc.printStackTrace();
	}
	return null;
}
 
private static String[] getEvents(int year, int month, boolean tenerife) {
	try {
		String pathRSS0 = "http://www.oan.es/servidorEfem/rss/"+year+"_spanish";
		if (Translate.getDefaultLanguage() == LANGUAGE.ENGLISH)
			pathRSS0 = "http://www.oan.es/servidorEfem/rss/"+year+"_english";
		if (tenerife) pathRSS0 += "_Tenerife";
		pathRSS0 += ".rss";
		String pathRSS = FileIO.getTemporalDirectory()+"feed.rss";
		GeneralQuery.queryFile(pathRSS0, pathRSS);
		String oldColumns[] = null, columns[] = null;
		ArrayList<String> out = new ArrayList<String>();
 
		Feed feed = Feed.readFeed(new URL("file://"+pathRSS));
		ArrayList<FeedMessageElement> mes = feed.getMessages();
 
		for (int i=0; i<mes.size(); i++) {
			FeedMessageElement m = mes.get(i);
 
			double jdEvent = (new AstroDate(m.pubDate)).jd();
			jdEvent = (int) (jdEvent - 0.5) + 0.5;
			AstroDate astro = new AstroDate(jdEvent);
			if (astro.getYear() == year && astro.getMonth() == month && m.title.indexOf("(M73)") < 0 && m.title.indexOf("(M40)") < 0) {
				if (m.link != null && !m.link.equals("")) {
					String l = m.link;
					int sep = l.indexOf("|");
					if (sep == 1) l = m.link.substring(2);
					String link = DataSet.replaceAll(l, "parent.mostrarApplet", "mostrarApplet", true);
					if (tenerife) link = DataSet.replaceAll(l, "Madrid", "Tenerife", true);
					int more = link.indexOf("|");
					if (more >= 0) {
						//String plus = link.substring(more+1);
						link = link.substring(0, more);
						//m.description += ". "+plus;
					}
					if (m.description.indexOf("J2000") > 0) link = DataSet.replaceAll(link, "skyloc_equ", "skyloc_eq0", true);
				}
 
				int val0 = m.description.lastIndexOf("(");
				int val1 = m.description.lastIndexOf(")");
				if (val1 > val0) {
					String bet = m.description.substring(val0+1, val1);
					int nf = FileIO.getNumberOfFields(bet, ",", false);
					if (nf == 3 || nf == 2) m.description = m.description.substring(0, val0) + "("+FileIO.getField(1, bet, ",", false).trim()+"%)";
				}
 
				columns = new String[] {
						m.title,
						m.description
				};
 
				if (oldColumns == null || !oldColumns[0].equals(columns[0]) || !oldColumns[1].equals(columns[1])) {
					out.add(DataSet.toString(columns, "@"));
				}
				oldColumns = columns;
			}
		}
 
		return DataSet.arrayListToStringArray(out);
	} catch (Exception exc) {
		return new String[0];
	}
}
 
private static String toUT(String s, int year, int month) throws Exception {
	if (s.equals("")) return s;
	int day = Integer.parseInt(FileIO.getField(1, s, " ", true));
	String t = FileIO.getField(2, s, " ", true);
	int h = Integer.parseInt(FileIO.getField(1, t, ":", true));
	int m = Integer.parseInt(FileIO.getField(2, t, ":", true));
	AstroDate astro = new AstroDate(year, month, day, h, m, 0);
	TimeElement time = new TimeElement(astro, SCALE.LOCAL_TIME);
	double jd = TimeScale.getJD(time, observer, eph, SCALE.UNIVERSAL_TIME_UTC);
	astro = new AstroDate(jd);
	String out = ""+astro.getDay()+" "+DateTimeOps.formatTime(AstroDate.getDayFraction(jd));
	if (s.indexOf("->") > 0) {
		String f = FileIO.getField(4, s, " ", true);
		if (f.indexOf("-") > 0) f = f.substring(f.lastIndexOf("-")+1);
		day = Integer.parseInt(f);
		t = FileIO.getField(5, s, " ", true);
		h = Integer.parseInt(FileIO.getField(1, t, ":", true));
		m = Integer.parseInt(FileIO.getField(2, t, ":", true));
		astro = new AstroDate(year, month, day, h, m, 0);
		time = new TimeElement(astro, SCALE.LOCAL_TIME);
		jd = TimeScale.getJD(time, observer, eph, SCALE.UNIVERSAL_TIME_UTC);
		astro = new AstroDate(jd);
		out += " -> "+astro.getDay()+" "+DateTimeOps.formatTime(AstroDate.getDayFraction(jd));			
	}
	return out;
}
 
private static String replaceGreek(String in) {
	String greek[] = DataSet.toStringArray("Alp,Bet,Gam,Del,Eps,Zet,Eta,The,Iot,Kap,Lam,Mu,Nu,Xi,Omi,Pi,Rho,Sig,Tau,Ups,Phi,Chi,Psi,Ome", ",", false);
	String[] latexGreek = new String[] {"alpha", "beta", "gamma",
	 "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa", "lambda", "mu", "nu",
	 "xi", "omicron", "pi", "rho", "sigma", "tau", "upsilon", "phi", "chi", "psi", "omega"};
	String out = in;
 
	for (int i=0; i<greek.length; i++) {
		out = DataSet.replaceAll(out, "("+greek[i]+" ", "($\\"+latexGreek[i]+"$ ", true);
		out = DataSet.replaceAll(out, "("+greek[i]+"1", "($\\"+latexGreek[i]+"$$_{1}$", true);
		out = DataSet.replaceAll(out, "("+greek[i]+"2", "($\\"+latexGreek[i]+"$$_{2}$", true);
		out = DataSet.replaceAll(out, "("+greek[i]+"3", "($\\"+latexGreek[i]+"$$_{3}$", true);
		out = DataSet.replaceAll(out, "("+greek[i]+"4", "($\\"+latexGreek[i]+"$$_{4}$", true);
	}
	return out;
}
 
private static void exportNavigationMapToPDFUsingiText(String plotFile, int i) {
	int w = 360, h = 75;
	Dimension size = new Dimension((int)(w * 4 - w / 10), (int)(h * 48) / 10);
	try {
		com.itextpdf.text.Rectangle pageSize = new com.itextpdf.text.Rectangle(0, 0, size.width, size.height);
		Document document = new Document(pageSize);
		PdfWriter writer = PdfSmartCopy.getInstance(document, new FileOutputStream(plotFile));
		writer.setCompressionLevel(9);
		writer.setFullCompression();
		document.open();
 
		// Create a custom font mapper that forces the use of the fonts we want
        FontMapper mapper = new FontMapper() {
        	String javaFonts[] = new String[] {"Dialog", "SansSerif", "Serif"};
            public BaseFont awtToPdf(Font font) {
                try {
                	int which = DataSet.getIndex(javaFonts, font.getName());
                	if (which < 0) {
                		which = 1;
                		System.out.println("Font "+font.getName()+" not found. Using "+javaFonts[which]+" instead.");
                	}
                    if (which == 2) return BaseFont.createFont(BaseFont.SYMBOL, BaseFont.CP1250, BaseFont.EMBEDDED);
 
                	String name = BaseFont.HELVETICA;
                	if (font.isBold()) name = BaseFont.HELVETICA_BOLD;
                	if (font.isItalic()) {
                		name = BaseFont.HELVETICA_OBLIQUE;
                		if (font.isBold()) name = BaseFont.HELVETICA_BOLDOBLIQUE;
                	}
                    return BaseFont.createFont(name, BaseFont.CP1252, BaseFont.EMBEDDED);
                } catch (DocumentException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }
            public Font pdfToAwt(BaseFont font, int size) { return null; }
        };
 
        PdfContentByte canvas = writer.getDirectContent();
        PdfTemplate template = canvas.createTemplate(size.width, size.height);
        canvas.addTemplate(template, 0, 0);
 
		// Vector graphics
        Graphics2D g = template.createGraphics(size.width, size.height, mapper, false, 1f); //canvas.createGraphics(pageSize.getWidth(), pageSize.getHeight(), false, 1f);
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, size.width, size.height);
 
		g.setColor(Color.BLUE);
		g.setFont(g.getFont().deriveFont(25f));
		int midx = size.width / 2, midy = size.height / 2;
 
		int ch = (int) ra0[i-1];
		int cd = (int) dec0[i-1];
		LocationElement loc = new LocationElement(ch / Constant.RAD_TO_HOUR, cd * Constant.DEG_TO_RAD, 1);
		LocationElement loc0 = loc.clone();
		String cons = getConstellations(new LocationElement(ch / Constant.RAD_TO_HOUR, cd * Constant.DEG_TO_RAD, 1), 2);
		String ral = "RA ", decl = "DEC ";
		ral = decl = "";
 
		g.translate(midx, midy);
		boolean spa = Translate.getDefaultLanguage() == LANGUAGE.SPANISH;
		String chart = spa ? "Carta " : "Chart ";
		doBox(g, w, h, chart+i, ral+ch+"h, "+decl+cd+"\u00b0", cons);
		g.translate(-midx, -midy);
		g.setColor(Color.BLACK);
 
		g.translate((midx + w - w / 3), midy);
		doArrow(g, w, h);
		g.translate(-(midx + w - w / 3), -midy);
 
		loc = loc0.clone();
		loc.toOffset(-45 * Constant.DEG_TO_RAD, 0);
		i = AtlasChart.skyAtlas2000(loc.getLongitude(), loc.getLatitude());
		ch = (int) ra0[i-1];
		cd = (int) dec0[i-1];
		cons = getConstellations(new LocationElement(ch / Constant.RAD_TO_HOUR, cd * Constant.DEG_TO_RAD, 1), 2);
 
		g.translate((midx + w * 2 - w / 2 - w / 15), midy);
		doBox(g, w, h, chart+i, ral+ch+"h, "+decl+cd+"\u00b0", cons);
		g.translate(-(midx + w * 2 - w / 2 - w / 15), -midy);
 
		g.translate((midx - w + w / 3), midy);
		g.rotate(Math.PI);
		doArrow(g, w, h);
		g.rotate(-Math.PI);
		g.translate(-(midx - w + w / 3), -midy);
 
		loc = loc0.clone();
		loc.toOffset(45 * Constant.DEG_TO_RAD, 0);
		i = AtlasChart.skyAtlas2000(loc.getLongitude(), loc.getLatitude());
		ch = (int) ra0[i-1];
		cd = (int) dec0[i-1];
		cons = getConstellations(new LocationElement(ch / Constant.RAD_TO_HOUR, cd * Constant.DEG_TO_RAD, 1), 2);
 
		g.translate((midx - w * 2 + w / 2 + w / 15), midy);
		doBox(g, w, h, chart+i, ral+ch+"h, "+decl+cd+"\u00b0", cons);
		g.translate(-(midx - w * 2 + w / 2 + w / 15), -midy);
 
		g.translate((midx), (midy - h + h / 3));
		g.rotate(-Constant.PI_OVER_TWO);
		doArrow(g, w, h);
		g.rotate(Constant.PI_OVER_TWO);
		g.translate(-(midx), -(midy - h + h / 3));
 
		loc = loc0.clone();
		loc.toOffset(0, 45 * Constant.DEG_TO_RAD);
		if (loc.getLatitude() > Constant.PI_OVER_TWO) {
			loc.setLatitude(Math.PI - loc.getLatitude());
			loc.move(Math.PI, 0, 0);
		}
		i = AtlasChart.skyAtlas2000(loc.getLongitude(), loc.getLatitude());
		ch = (int) ra0[i-1];
		cd = (int) dec0[i-1];
		cons = getConstellations(new LocationElement(ch / Constant.RAD_TO_HOUR, cd * Constant.DEG_TO_RAD, 1), 2);
 
		g.translate((midx), midy - h * 2 + h / 8);
		doBox(g, w, h, chart+i, ral+ch+"h, "+decl+cd+"\u00b0", cons);
		g.translate(-(midx), -(midy - h * 2 + h / 8));
 
		g.translate((midx), (midy - (-h + h / 3)));
		g.rotate(Constant.PI_OVER_TWO);
		doArrow(g, w, h);
		g.rotate(-Constant.PI_OVER_TWO);
		g.translate(-(midx), -(midy - (-h + h / 3)));
 
		loc = loc0.clone();
		loc.toOffset(0, -45 * Constant.DEG_TO_RAD);
		if (loc.getLatitude() < -Constant.PI_OVER_TWO) {
			loc.setLatitude(-Math.PI - loc.getLatitude());
			loc.move(Math.PI, 0, 0);
		}
		i = AtlasChart.skyAtlas2000(loc.getLongitude(), loc.getLatitude());
		ch = (int) ra0[i-1];
		cd = (int) dec0[i-1];
		cons = getConstellations(new LocationElement(ch / Constant.RAD_TO_HOUR, cd * Constant.DEG_TO_RAD, 1), 2);
 
		g.translate((midx), midy - (-h * 2 + h / 8));
		doBox(g, w, h, chart+i, ral+ch+"h, "+decl+cd+"\u00b0", cons);
		g.translate(-(midx), -(midy - (-h * 2 + h / 8)));
 
		g.dispose();
 
		document.close();
	} catch (Exception exc) {
		exc.printStackTrace();
	}
}
 
private static void doBox(Graphics2D g, int w, int h, String s, String p, String c) {
	g.drawLine(-w/2, -h/2, w/2, -h/2);
	g.drawLine(-w/2, -h/2, -w/2, h/2);
	g.drawLine(w/2, -h/2, w/2, h/2);
	g.drawLine(-w/2, h/2, w/2, h/2);
	int fs = g.getFont().getSize();
	g.setFont(g.getFont().deriveFont(Font.BOLD));
	String s1 = s + " ("+p+")";
	g.drawString(s1, -g.getFontMetrics().stringWidth(s1)/2, -h/2 + (fs*5)/4);
	g.setFont(g.getFont().deriveFont(Font.PLAIN));
	g.drawString(c, -g.getFontMetrics().stringWidth(c)/2, -h/2 + (fs*260)/100);
	//g.drawString(c, -g.getFontMetrics().stringWidth(c)/2, -h/2 + (fs*9)/2);
}
 
private static void doArrow(Graphics2D g, int w, int h) {
	Color col = g.getColor();
	g.setColor(Color.RED);
	g.drawLine(-0*w/20, -h/6, -0*w/20, h/6);
	g.drawLine(-0*w/20, -h/6, w/16, -h/6);
	g.drawLine(-0*w/20, h/6, w/16, h/6);
	g.drawLine(w/16, -h/6, w/16, -h/3);
	g.drawLine(w/16, h/6, w/16, h/3);
	g.drawLine(w/16, h/3, w/8, 0);
	g.drawLine(w/16, -h/3, w/8, 0);
	g.setColor(col);
}
 
private static String getConstellations(LocationElement loc0, int n) throws Exception {
	String cons = Constellation.getConstellationName(loc0, epoch, eph);
	if (Translate.getDefaultLanguage() == LANGUAGE.SPANISH && sky.drawConstellationNamesType == CONSTELLATION_NAME.SPANISH)
		cons = Constellation.getConstellation(cons, CONSTELLATION_NAME.SPANISH);
	if (n == 1) return cons;
	int c = 1;
	for (int r=5;r<=40;r=r+10) {
		for (int a=0;a<360;a=a+45) {
			LocationElement loc = loc0.clone();
			double dlon = r * Math.cos(a * Constant.DEG_TO_RAD), dlat = r * Math.sin(a * Constant.DEG_TO_RAD);
			loc.toOffset(dlon * Constant.DEG_TO_RAD, dlat * Constant.DEG_TO_RAD);
			String cons1 = Constellation.getConstellationName(loc, epoch, eph);
			if (Translate.getDefaultLanguage() == LANGUAGE.SPANISH && sky.drawConstellationNamesType == CONSTELLATION_NAME.SPANISH)
				cons1 = Constellation.getConstellation(cons1, CONSTELLATION_NAME.SPANISH);
			if (cons.indexOf(cons1) < 0) {
				cons += ", "+cons1;
					c ++;
					if (n > 0 && c >= n) break;
				}
			}
			if (n > 0 && c >= n) break;
		}
		return cons;
	}
}

Rendering the sky like Stellarium with JPARSEC

There is an old project called Stellarium for Java. It is a port of the old 0.8 version of Stellarium, which is a very popular planetarium due to its high quality, realistic renderings. This project uses the JOGL library to offer a similar experience to the original using Java, but unfortunately it was later abandoned.

I have recently analized the original code to look how this port renders the sky, in particular how it computes the atmosphere brightness and the sky colors considering the position of the Sun or the Moon. I have taken just this code and connected it to the rendering process used in JPARSEC, so that similar renderings can be done (pixel by pixel, so slower) without using the GPU. For this task I have updated JPARSEC with a few improvements (availables from the JPARSEC repository at bitbucket) to improve the horizon rendering with terrain, adding two more more textures.

The idea is to render the sky with JPARSEC using a given texture for the horizon and a transparent image, and reuse the SkyRendering object to compute, pixel by pixel, the illumination and color of the background atmosphere using the formulae from Stellarium. With this you only have to draw the sky image on top of the atmosphere image for the final image. So here are the great results…




 

I leave to you the task of analyzing the code to try to understand the underlying maths and physics, I prefer not to describe it since I'm not an expert, I have just borrowed the code. Here is the rather long Java class that can be used for such renderings in case you want to play with it.

StellariumAtmosphere.java
import static java.lang.StrictMath.PI;
import static java.lang.StrictMath.abs;
import static java.lang.StrictMath.acos;
import static java.lang.StrictMath.asin;
import static java.lang.StrictMath.cos;
import static java.lang.StrictMath.exp;
import static java.lang.StrictMath.log;
import static java.lang.StrictMath.log10;
import static java.lang.StrictMath.pow;
import static java.lang.StrictMath.sin;
import static java.lang.StrictMath.tan;
 
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
 
import jparsec.astronomy.TelescopeElement;
import jparsec.astronomy.CoordinateSystem.COORDINATE_SYSTEM;
import jparsec.ephem.Ephem;
import jparsec.ephem.EphemerisElement;
import jparsec.ephem.Functions;
import jparsec.ephem.Target.TARGET;
import jparsec.ephem.planets.EphemElement;
import jparsec.graph.chartRendering.AWTGraphics;
import jparsec.graph.chartRendering.Graphics;
import jparsec.graph.chartRendering.PlanetRenderElement;
import jparsec.graph.chartRendering.SkyRenderElement;
import jparsec.graph.chartRendering.Projection.PROJECTION;
import jparsec.graph.chartRendering.RenderPlanet;
import jparsec.graph.chartRendering.SkyRenderElement.HORIZON_TEXTURE;
import jparsec.graph.chartRendering.SkyRenderElement.LEYEND_POSITION;
import jparsec.graph.chartRendering.SkyRenderElement.MILKY_WAY_TEXTURE;
import jparsec.graph.chartRendering.SkyRenderElement.REALISTIC_STARS;
import jparsec.graph.chartRendering.frame.SkyRendering;
import jparsec.io.ConsoleReport;
import jparsec.io.image.Picture;
import jparsec.math.Constant;
import jparsec.math.FastMath;
import jparsec.observer.City;
import jparsec.observer.LocationElement;
import jparsec.observer.ObserverElement;
import jparsec.time.AstroDate;
import jparsec.time.TimeElement;
 
public class StellariumAtmosphere {
 
	public static SkyRendering render;
 
	// Returns an image showing the sky for a given fixed set of properties: observer, appearance, and so on
	public static BufferedImage getStarsImage() throws Exception {        	
    	// Parameters for the rendering: geographical location and elevation, time zone,
    	// camera pointing direction and inclination, sensor size in pixels, field of view, ...
    	String locName = "Madrid";
    	double cameraAcimut = 90 * Constant.DEG_TO_RAD; // 0 = North
    	double cameraElevation = 50 * Constant.DEG_TO_RAD; // 0 = horizon, 90 = zenith
    	double cameraInclination = 0 * Constant.DEG_TO_RAD;
    	int cameraPixelX = 1200, cameraPixelY = 800;
    	double fov = 120 * Constant.DEG_TO_RAD;
    	TimeElement.SCALE ts = TimeElement.SCALE.LOCAL_TIME;
    	AstroDate astro = new AstroDate(2018, 2, 19, 10, 0, 0);
    	TimeElement date = new TimeElement(astro, ts);
 
    	EphemerisElement eph = new EphemerisElement(TARGET.SUN,
    		EphemerisElement.COORDINATES_TYPE.APPARENT, EphemerisElement.EQUINOX_OF_DATE,
    		EphemerisElement.TOPOCENTRIC, EphemerisElement.REDUCTION_METHOD.IAU_2006,
    		EphemerisElement.FRAME.DYNAMICAL_EQUINOX_J2000,
    		EphemerisElement.ALGORITHM.MOSHIER);
    	eph.optimizeForSpeed();
    	ObserverElement observer = ObserverElement.parseCity(City.findCity(locName));
 
    	SkyRenderElement sky = getSky(cameraAcimut, cameraElevation, cameraPixelX, cameraPixelY, fov, cameraInclination);
 
	sky.drawMilkyWayContoursWithTextures = MILKY_WAY_TEXTURE.OPTICAL;
	EphemElement ephemSun = Ephem.getEphemeris(date, observer, eph, true);
	if (ephemSun.elevation > -5 * Constant.DEG_TO_RAD) sky.drawMilkyWayContoursWithTextures = MILKY_WAY_TEXTURE.NO_TEXTURE;
	sky.drawMilkyWayContours = sky.drawMilkyWayContoursWithTextures == MILKY_WAY_TEXTURE.NO_TEXTURE ? false : true;
	int yMargin = 0;
 
	// These lines are used to change the vertical position of the horizon, to create panoramas
//		sky.height = sky.width/2+150;
//		yMargin = sky.height/2-100;
 
    	render = new SkyRendering(date, observer, eph, sky, locName, yMargin);  
    	// JPARSEC supports transparency using these two lines
    	AWTGraphics.setBufferedImageType(BufferedImage.TYPE_INT_ARGB);
    	BufferedImage out = render.createTransparentBufferedImage();
    	return out;
    }
 
    public static BufferedImage getSkyBrightness() throws Exception {
	EphemElement ephemSun = render.getRenderSkyObject().calcPlanet(TARGET.SUN, true, true);
	EphemElement ephemMoon = render.getRenderSkyObject().calcPlanet(TARGET.Moon, true, false);
 
	ConsoleReport.fullEphemReportToConsole(ephemSun);
 
	ToneReproductor eye = new ToneReproductor();
	StellariumAtmosphere atmosphere = new StellariumAtmosphere();
	float atmBr = 0.0001f, turbidity = 1f, atmI = 1000f;
	if (ephemSun.elevation < 0) {
		double elev = Math.min(18, Math.abs(ephemSun.elevation * Constant.RAD_TO_DEG));
		atmI = (float) (1000 + 5000 * elev / 15.0);
		//turbidity = (float) (2 - 1 * Math.abs(ephemSun.elevation * Constant.RAD_TO_DEG) / 15.0);
	}
        eye.setWorldAdaptationLuminance(3.75f + atmBr * 40000.f);
        atmosphere.atmIntensity = 255 * atmBr * atmI;			
 
        int year = render.getTimeObject().astroDate.getYear(), month = render.getTimeObject().astroDate.getMonth();
        double latitude = render.getObserverObject().getLatitudeDeg(), altitude = render.getObserverObject().getHeight(), 
        		temperature = render.getObserverObject().getTemperature(), relativeHumidity = render.getObserverObject().getHumidity();
        double moonPhase = Math.PI - ephemMoon.elongation;
 
        LocationElement sunLoc = new LocationElement(ephemSun.azimuth, ephemSun.elevation, 1);
        LocationElement moonLoc = new LocationElement(ephemMoon.azimuth, ephemMoon.elevation, 1);
        LocationElement zenLoc = new LocationElement(0, Constant.PI_OVER_TWO, 1);
	double moonZenD = LocationElement.getAngularDistance(moonLoc, zenLoc);
	double sunZenD = LocationElement.getAngularDistance(sunLoc, zenLoc);
 
	atmosphere.skyLight.setParams((float) sunZenD, turbidity);
	atmosphere.skyBright.setLoc(Math.toRadians(latitude), altitude, temperature, relativeHumidity);
	atmosphere.skyBright.setSunMoon(Math.cos(moonZenD), Math.cos(sunZenD));
 
	atmosphere.skyBright.setDate(year, month, moonPhase);
 
	int w = render.getRenderSkyObject().render.width;
	int h = render.getRenderSkyObject().render.height;
	BufferedImage out = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
	int black = (new java.awt.Color(0, 0, 0)).getRGB();
	for (int x = 0; x < w; ++x) {
	for (int y = 0; y < h; ++y) {
		LocationElement skyLoc = render.getRenderSkyObject().getSkyLocation(x, y);
		if (skyLoc.getLatitude() <= 0) {
			out.setRGB(x, y, black);
			continue;
		}
		double sunD = LocationElement.getApproximateAngularDistance(skyLoc, sunLoc); 
		double moonD = LocationElement.getApproximateAngularDistance(skyLoc, moonLoc); 
		double zenD = LocationElement.getApproximateAngularDistance(skyLoc, zenLoc); 
 
		double cosDistSun = FastMath.cos(sunD), cosDistZen = FastMath.cos(zenD);
		float color[] = atmosphere.skyLight.get_xyY_valuev(cosDistSun, 1.0 / cosDistZen);
		color[2] = (float) atmosphere.skyBright.getLuminance(FastMath.cos(moonD), 
			cosDistSun, cosDistZen);
 
		eye.xyYToRGB(color);
 
		int r = (int) (atmosphere.atmIntensity * color[0]);
		int g = (int) (atmosphere.atmIntensity * color[1]);
		int b = (int) (atmosphere.atmIntensity * color[2]);
		if (r > 255) r = 255;
		if (g > 255) g = 255;
		if (b > 255) b = 255;
		if (r < 0) r = 0;
		if (g < 0) g = 0;
		if (b < 0) b = 0;
		int rgb = (new java.awt.Color(r, g, b)).getRGB();
		out.setRGB(x, y, rgb);
	}
	}
	return out;
    }
 
    // Returns the sky render object containing all properties to define the rendering appearance
    public static SkyRenderElement getSky(double acimut, double elevation, int width, int height, double fov, double incl) {
    	PlanetRenderElement planet = new PlanetRenderElement(false, true, true, false);
	SkyRenderElement sky = new SkyRenderElement(COORDINATE_SYSTEM.HORIZONTAL,
		PROJECTION.STEREOGRAPHICAL, acimut, elevation, width, height, planet, 
		TelescopeElement.OBJECTIVE_50mm_f1_4);
	sky.telescope.ocular.focalLength = TelescopeElement.getOcularFocalLengthForCertainField(fov, sky.telescope);
	sky.drawObjectsLimitingMagnitude = 3f;        
	sky.drawStarsLimitingMagnitude = 5.0f;
	sky.drawPlanetsMoonSun = true;
	sky.drawSkyCorrectingLocalHorizon = true;        
	sky.drawSkyBelowHorizon = false;        
	sky.drawFastLabels = SkyRenderElement.SUPERIMPOSED_LABELS.AVOID_SUPERIMPOSING_ACCURATE;        
	sky.drawFastLabelsInWideFields = false;        
	sky.drawConstellationLimits = false;
	sky.drawLeyend = LEYEND_POSITION.TOP;
	sky.drawExternalGrid = false;
	sky.drawCoordinateGrid = false;
	sky.drawDeepSkyObjectsAllMessierAndCaldwell = false;
	sky.drawStarsRealistic = REALISTIC_STARS.SPIKED;
	sky.drawMeteorShowers = false;
	sky.drawNebulaeContours = false;
	sky.drawComets = sky.drawAsteroids = false;
	sky.drawConstellationNames = false;
	sky.poleAngle = (float) incl;
	sky.drawStarsLabelsLimitingMagnitude = 1.1f;
	sky.drawPlanetsMoonSun = true;
	sky.drawPlanetsLabels = true;
	sky.drawIcons = false;
	sky.drawSpaceProbes = false;
	sky.drawDeepSkyObjects = false;
	sky.drawConstellationNames = true;
	//sky.drawConstellationNamesType = CONSTELLATION_NAME.SPANISH;
	sky.drawConstellationNamesFont = Graphics.FONT.SANS_SERIF_ITALIC_22;
	sky.drawPlanetsNamesFont = Graphics.FONT.SANS_SERIF_BOLD_16;
	sky.drawStarsNamesFont = Graphics.FONT.DIALOG_PLAIN_12;
	sky.drawDeepSkyObjectsTextures = false;
	sky.drawCoordinateGridEclipticLabels = false;
	sky.planetRender.textures = false;
	sky.drawMilkyWayContoursWithTextures = MILKY_WAY_TEXTURE.OPTICAL;
	sky.drawMilkyWayContours = sky.drawMilkyWayContoursWithTextures == MILKY_WAY_TEXTURE.NO_TEXTURE ? false : true;
	sky.drawHorizonTexture = HORIZON_TEXTURE.VELETA_30m;
	sky.drawLeyend = LEYEND_POSITION.NO_LEYEND;
	sky.planetRender.highQuality = true;
	RenderPlanet.MAXIMUM_TEXTURE_QUALITY_FACTOR = 4f;
 
	sky.setColorMode(SkyRenderElement.COLOR_MODE.BLACK_BACKGROUND);
	sky.background = Functions.getColor(0, 0, 0, 0);
	return sky;
    }
 
    private Skylight skyLight = new Skylight();
 
    private SkyBright skyBright = new SkyBright();
 
    double worldAdaptationLuminance;
 
    double milkywayAdaptationLuminance;
 
    private float atmIntensity;
 
    public static void main(String args[]) {
    	try {
        	//Translate.setDefaultLanguage(LANGUAGE.SPANISH);
 
    		BufferedImage sky = getStarsImage();
    		Picture pic = new Picture(getSkyBrightness());
    		Graphics2D g = pic.getImage().createGraphics();
    		AWTGraphics.enableAntialiasing(g);
    		g.drawImage(sky, 0, 0, null);
    		g.dispose();
    		pic.show("");
    		pic.write("/home/alonso/test.png");
 
    	} catch (Exception exc) {
    		exc.printStackTrace();
    	}
    }
}
 
class ToneReproductor {
 
   public ToneReproductor() {
        lda = 50.f;
        lwa = 40000.f;
        maxDL = 100.f;
        gamma = 2.3f;
 
        // Update alpha_da and beta_da values
        double log10Lwa = log10(lwa);
        alphaWa = 0.4f * log10Lwa + 1.519f;
        betaWa = -0.4f * log10Lwa * log10Lwa + 0.218f * log10Lwa + 6.1642f;
 
        setDisplayAdaptationLuminance(lda);
        setWorldAdaptationLuminance(lwa);
    }
 
    /**
     * Set the eye adaptation luminance for the display and precompute what can be
     * Usual luminance range is 1-100 cd/m^2 for a CRT screen
     *
     * @param _Lda
     */
    void setDisplayAdaptationLuminance(double _Lda) {
        lda = _Lda;
 
        // Update alpha_da and beta_da values
        double log10Lda = log10(lda);
        alphaDa = 0.4f * log10Lda + 1.519f;
        betaDa = -0.4f * log10Lda * log10Lda + 0.218f * log10Lda + 6.1642f;
 
        // Update terms
        alphaWaOverAlphaDa = alphaWa / alphaDa;
        term2 = pow(10.f, (betaWa - betaDa) / alphaDa) / (PI * 0.0001f);
    }
 
    /**
     * Set the eye adaptation luminance for the world and precompute what can be
     *
     * @param _Lwa
     */
    public void setWorldAdaptationLuminance(double _Lwa) {
        lwa = _Lwa;
 
        // Update alpha_da and beta_da values
        double log10Lwa = log10(lwa);
        alphaWa = 0.4f * log10Lwa + 1.519f;
        betaWa = -0.4f * log10Lwa * log10Lwa + 0.218f * log10Lwa + 6.1642f;
 
        // Update terms
        alphaWaOverAlphaDa = alphaWa / alphaDa;
        term2 = pow(10.f, (betaWa - betaDa) / alphaDa) / (PI * 0.0001f);
 
    }
 
    /**
     * Convert from xyY color system to RGB according to the adaptation
     * The Y component is in cd/m^2
     */
    public void xyYToRGB(float[] color) {
        // TODO: Fred the parameter should an SColor object
        // 1. Hue conversion
        float log10Y = (float) log10(color[2]);
        // if log10Y>0.6, photopic vision only (with the cones, colors are seen)
        // else scotopic vision if log10Y<-2 (with the rods, no colors, everything blue),
        // else mesopic vision (with rods and cones, transition state)
        if (log10Y < 0.6) {
            // Compute s, ratio between scotopic and photopic vision
            float s = 0.f;
            if (log10Y > -2.f) {
                float op = (log10Y + 2.f) / 2.6f;
                s = 3.f * op * op - 2 * op * op * op;
            }
 
            // Do the blue shift for scotopic vision simulation (night vision) [3]
            // The "night blue" is x,y(0.25, 0.25)
            color[0] = (1.f - s) * 0.25f + s * color[0];// Add scotopic + photopic components
            color[1] = (1.f - s) * 0.25f + s * color[1];// Add scotopic + photopic components
 
            // Take into account the scotopic luminance approximated by V [3] [4]
            double V = color[2] * (1.33f * (1.f + color[1] / color[0] + color[0] * (1.f - color[0] - color[1])) - 1.68f);
            color[2] = (float) (0.4468f * (1.f - s) * V + s * color[2]);
        }
 
        // 2. Adapt the luminance value and scale it to fit in the RGB range [2]
        color[2] = (float) pow(adaptLuminance(color[2]) / maxDL, 1.d / gamma);
 
        // Convert from xyY to XZY
        double X = color[0] * color[2] / color[1];
        double Y = color[2];
        double Z = (1.f - color[0] - color[1]) * color[2] / color[1];
 
        // Use a XYZ to Adobe RGB (1998) matrix which uses a D65 reference white
        color[0] = (float) (2.04148f * X - 0.564977f * Y - 0.344713f * Z);
        color[1] = (float) (-0.969258f * X + 1.87599f * Y + 0.0415557f * Z);
        color[2] = (float) (0.0134455f * X - 0.118373f * Y + 1.01527f * Z);
    }
 
    /**
     * Set the maximum display luminance : default value = 100 cd/m^2
     * This value is used to scale the RGB range
     */
    void set_max_display_luminance(float _maxdL) {
        maxDL = _maxdL;
    }
 
    /**
     * Set the display gamma : default value = 2.3
     */
    void setDisplayGamma(float _gamma) {
        gamma = _gamma;
    }
 
    /**
     * Return adapted luminance from world to display
     */
    public double adaptLuminance(double worldLluminance) {
        return pow(worldLluminance * PI * 0.0001d, alphaWaOverAlphaDa) * term2;
    }
 
    private double lda;// Display luminance adaptation (in cd/m^2)
 
    double lwa;// World   luminance adaptation (in cd/m^2)
 
    double maxDL;// Display maximum luminance (in cd/m^2)
 
    double gamma;// Screen gamma value
 
    // Precomputed variables
    double alphaDa;
 
    double betaDa;
 
    double alphaWa;
 
    double betaWa;
 
    double alphaWaOverAlphaDa;
 
    double term2;
 
}
 
class Skylight {
    public class SkylightStruct {
        double zenith_angle;// zenith_angle : angular distance to the zenith in radian
 
        double dist_sun;// dist_sun     : angular distance to the sun in radian
 
        double color[] = new double[3];// 3 component color, can be RGB or CIE color system
    }
 
    public static class SkylightStruct2 {
        public double pos[] = new double[3];// Vector to the position (vertical = pos[2])
 
        public float color[] = new float[3];// 3 component color, can be RGB or CIE color system
    }
 
    public Skylight() {
    }
 
    void setParams(float _sun_zenith_angle, float _turbidity) {
        // Set the two Main variables
        thetas = _sun_zenith_angle;
        T = _turbidity;
 
        // Precomputation of the distribution coefficients and zenith luminances/color
        compute_zenith_luminance();
        compute_zenith_color();
        compute_luminance_distribution_coefs();
        compute_color_distribution_coefs();
 
        // Precompute everything possible to increase the get_CIE_value() function speed
        double cos_thetas = cos(thetas);
        termX = (float) (zenithColorX / ((1.f + aX * exp(bX)) * (1.f + cX * exp(dX * thetas) + eX * cos_thetas * cos_thetas)));
        termY = (float) (zenith_color_y / ((1.f + Ay * exp(By)) * (1.f + Cy * exp(Dy * thetas) + Ey * cos_thetas * cos_thetas)));
        term_Y = (float) (zenith_luminance / ((1.f + AY * exp(BY)) * (1.f + CY * exp(DY * thetas) + EY * cos_thetas * cos_thetas)));
 
    }
 
    public void setParamsV(float[] _sun_pos, float _turbidity) {
        // Store sun position
        sunPos[0] = _sun_pos[0];
        sunPos[1] = _sun_pos[1];
        sunPos[2] = _sun_pos[2];
 
        // Set the two Main variables
        thetas = (float) (PI / 2 - asin(sunPos[2]));
        T = _turbidity;
 
        // Precomputation of the distribution coefficients and zenith luminances/color
        compute_zenith_luminance();
        compute_zenith_color();
        compute_luminance_distribution_coefs();
        compute_color_distribution_coefs();
 
        // Precompute everything possible to increase the get_CIE_value() function speed
        double cos_thetas = sunPos[2];
        termX = (float) (zenithColorX / ((1.f + aX * exp(bX)) * (1.f + cX * exp(dX * thetas) + eX * cos_thetas * cos_thetas)));
        termY = (float) (zenith_color_y / ((1.f + Ay * exp(By)) * (1.f + Cy * exp(Dy * thetas) + Ey * cos_thetas * cos_thetas)));
        term_Y = (float) (zenith_luminance / ((1.f + AY * exp(BY)) * (1.f + CY * exp(DY * thetas) + EY * cos_thetas * cos_thetas)));
    }
 
    /**
     * Compute CIE luminance for zenith in cd/m^2
     */
    void compute_zenith_luminance() {
        zenith_luminance = (float) (1000.f * ((4.0453f * T - 4.9710f) * tan((0.4444f - T / 120.f) * (PI - 2.f * thetas)) -
                0.2155f * T + 2.4192f));
        if (zenith_luminance <= 0.f) zenith_luminance = 0.00000000001f;
    }
 
    /**
     * Compute CIE x and y color components
     */
    void compute_zenith_color() {
        thetas2 = thetas * thetas;
        thetas3 = thetas2 * thetas;
        T2 = T * T;
 
        zenithColorX = (0.00166f * thetas3 - 0.00375f * thetas2 + 0.00209f * thetas) * T2 +
                (-0.02903f * thetas3 + 0.06377f * thetas2 - 0.03202f * thetas + 0.00394f) * T +
                (0.11693f * thetas3 - 0.21196f * thetas2 + 0.06052f * thetas + 0.25886f);
 
        zenith_color_y = (0.00275f * thetas3 - 0.00610f * thetas2 + 0.00317f * thetas) * T2 +
                (-0.04214f * thetas3 + 0.08970f * thetas2 - 0.04153f * thetas + 0.00516f) * T +
                (0.15346f * thetas3 - 0.26756f * thetas2 + 0.06670f * thetas + 0.26688f);
 
    }
 
    /**
     * Compute the luminance distribution coefficients
     */
    void compute_luminance_distribution_coefs() {
        AY = 0.1787f * T - 1.4630f;
        BY = -0.3554f * T + 0.4275f;
        CY = -0.0227f * T + 5.3251f;
        DY = 0.1206f * T - 2.5771f;
        EY = -0.0670f * T + 0.3703f;
    }
 
    /**
     * Compute the color distribution coefficients
     */
    void compute_color_distribution_coefs() {
        aX = -0.0193f * T - 0.2592f;
        bX = -0.0665f * T + 0.0008f;
        cX = -0.0004f * T + 0.2125f;
        dX = -0.0641f * T - 0.8989f;
        eX = -0.0033f * T + 0.0452f;
 
        Ay = -0.0167f * T - 0.2608f;
        By = -0.0950f * T + 0.0092f;
        Cy = -0.0079f * T + 0.2102f;
        Dy = -0.0441f * T - 1.6537f;
        Ey = -0.0109f * T + 0.0529f;
    }
 
    /**
     * Compute the sky color at the given position in the CIE color system and store it in p.color
     * p.color[0] is CIE x color component
     * p.color[1] is CIE y color component
     * p.color[2] is CIE Y color component (luminance)
     */
    void get_xyY_value(SkylightStruct p) {
        double cos_dist_sun = cos(p.dist_sun);
        double one_over_cos_zenith_angle = 1.f / cos(p.zenith_angle);
        p.color[0] = termX * (1.f + aX * exp(bX * one_over_cos_zenith_angle)) * (1.f + cX * exp(dX * p.dist_sun) +
                eX * cos_dist_sun * cos_dist_sun);
        p.color[1] = termY * (1.f + Ay * exp(By * one_over_cos_zenith_angle)) * (1.f + Cy * exp(Dy * p.dist_sun) +
                Ey * cos_dist_sun * cos_dist_sun);
        p.color[2] = term_Y * (1.f + AY * exp(BY * one_over_cos_zenith_angle)) * (1.f + CY * exp(DY * p.dist_sun) +
                EY * cos_dist_sun * cos_dist_sun);
    }
 
    /**
     * Compute the sky color at the given position in the CIE color system and store it in p.color
     * p.color[0] is CIE x color component
     * p.color[1] is CIE y color component
     * p.color[2] is CIE Y color component (luminance)
     */
    public void get_xyY_valuev(SkylightStruct2 p) {
        //	if (p.pos[2]<0.)
        //	{
        //		p.color[0] = 0.25;
        //		p.color[1] = 0.25;
        //		p.color[2] = 0;
        //		return;
        //	}
 
        double cosDistSun = sunPos[0] * (p.pos[0]) + sunPos[1] * (p.pos[1]) + sunPos[2] * (p.pos[2]) - 0.0000001f;
        double oneOverCosZenithAngle = 1.f / p.pos[2];
        float distSun = (float) acos(cosDistSun);
 
        p.color[0] = (float) (termX * (1.f + aX * exp(bX * oneOverCosZenithAngle)) * (1.f + cX * exp(dX * distSun) +
                eX * cosDistSun * cosDistSun));
        p.color[1] = (float) (termY * (1.f + Ay * exp(By * oneOverCosZenithAngle)) * (1.f + Cy * exp(Dy * distSun) +
                Ey * cosDistSun * cosDistSun));
        p.color[2] = (float) (term_Y * (1.f + AY * exp(BY * oneOverCosZenithAngle)) * (1.f + CY * exp(DY * distSun) +
                EY * cosDistSun * cosDistSun));
 
        if (p.color[2] < 0 || p.color[0] < 0 || p.color[1] < 0) {
            p.color[0] = 0.25f;
            p.color[1] = 0.25f;
            p.color[2] = 0;
        }
    }
 
    /**
     * Compute the sky color at the given position in the CIE color system and store it in p.color
     * p.color[0] is CIE x color component
     * p.color[1] is CIE y color component
     * p.color[2] is CIE Y color component (luminance)
     */
    public float[] get_xyY_valuev(double cosDistSun, double oneOverCosZenithAngle) {
        //	if (p.pos[2]<0.)
        //	{
        //		p.color[0] = 0.25;
        //		p.color[1] = 0.25;
        //		p.color[2] = 0;
        //		return;
        //	}
 
        float distSun = (float) FastMath.acos(cosDistSun);
 
        float color[] = new float[3];
        color[0] = (float) (termX * (1.f + aX * exp(bX * oneOverCosZenithAngle)) * (1.f + cX * exp(dX * distSun) +
                eX * cosDistSun * cosDistSun));
        color[1] = (float) (termY * (1.f + Ay * exp(By * oneOverCosZenithAngle)) * (1.f + Cy * exp(Dy * distSun) +
                Ey * cosDistSun * cosDistSun));
        color[2] = (float) (term_Y * (1.f + AY * exp(BY * oneOverCosZenithAngle)) * (1.f + CY * exp(DY * distSun) +
                EY * cosDistSun * cosDistSun));
 
        if (color[2] < 0 || color[0] < 0 || color[1] < 0) {
            color[0] = 0.25f;
            color[1] = 0.25f;
            color[2] = 0;
        }
        return color;
    }
 
    /**
     * Return the current zenith color in xyY color system
     */
    void get_zenith_color(double[] v) {
        v[0] = zenithColorX;
        v[1] = zenith_color_y;
        v[2] = zenith_luminance;
    }
 
    private float thetas;// angular distance between the zenith and the sun in radian
 
    float T;// Turbidity : i.e. sky "clarity"
 
    //  1 : pure air
    //  2 : exceptionnally clear
    //  4 : clear
    //  8 : light haze
    // 25 : haze
    // 64 : thin fog
 
    // Computed variables depending on the 2 above
 
    float zenith_luminance;// Y color component of the CIE color at zenith (luminance)
 
    float zenithColorX;// x color component of the CIE color at zenith
 
    float zenith_color_y;// y color component of the CIE color at zenith
 
    double eye_lum_conversion;// luminance conversion for an eye adapted to screen luminance (around 40 cd/m^2)
 
    double AY, BY, CY, DY, EY;// Distribution coefficients for the luminance distribution function
 
    float aX, bX, cX, dX, eX;// Distribution coefficients for x distribution function
 
    double Ay, By, Cy, Dy, Ey;// Distribution coefficients for y distribution function
 
    float termX;// Precomputed term for x calculation
 
    float termY;// Precomputed term for y calculation
 
    float term_Y;// Precomputed term for luminance calculation
 
    float sunPos[] = new float[3];
 
    static float thetas2;
 
    static float thetas3;
 
    static float T2;
}
 
class SkyBright {
    public SkyBright() {
        setDate(2003, 8, 0);
        setLoc(Constant.PI_OVER_FOUR, 1000.d, 25.d, 40.d);
        setSunMoon(0.5, 0.5);
    }
 
    /**
     * @param year
     * @param month     1=Jan, 12=Dec
     * @param moonPhase in radian 0=Full Moon, PI/2=First Quadrant/Last Quadran, PI=No Moon
     */
    public void setDate(int year, int month, double moonPhase) {
        magMoon = -12.73d + 1.4896903d * abs(moonPhase) + 0.04310727d * pow(moonPhase, 4.d);
 
        RA = (month - 3.d) * 0.52359878d;
 
        // Term for dark sky brightness computation
        bNightTerm = 1.0e-13 + 0.3e-13 * cos(0.56636d * (year - 1992.d));
    }
 
 
    public void setLoc(double latitude, double altitude, double temperature, double relativeHumidity) {
        double signLatitude = (latitude >= 0.d) ? 2.d - 1.d : 0 - 1.d;
 
        // extinction Coefficient for V band
        double KR = 0.1066d * exp(-altitude / 8200.d);
        double KA = 0.1d * exp(-altitude / 1500.d) * pow(1.d - 0.32d / log(relativeHumidity / 100.d), 1.33d) *
                (1.d + 0.33d * signLatitude * sin(RA));
        double KO = 0.031d * (3.d + 0.4d * (latitude * cos(RA) - cos(3.d * latitude))) / 3.d;
        double KW = 0.031d * 0.94d * (relativeHumidity / 100.d) * exp(temperature / 15.d) * exp(-altitude / 8200.d);
        K = KR + KA + KO + KW;
    }
 
    /**
     * Set the moon and sun zenith angular distance (cosin given) and precompute what can be
     *
     * @param cosDistMoonZenith
     * @param cosDistSunZenith
     */
    public void setSunMoon(double cosDistMoonZenith, double cosDistSunZenith) {
        // Air mass for Moon
        if (cosDistMoonZenith < 0) airMassMoon = 40.f;
        else airMassMoon = 1.f / (cosDistMoonZenith + 0.025f * exp(-11.f * cosDistMoonZenith));
 
        // Air mass for Sun
        if (cosDistSunZenith < 0) airMassSun = 40;
        else airMassSun = 1.f / (cosDistSunZenith + 0.025f * exp(-11.f * cosDistSunZenith));
 
        bMoonTerm1 = pow(10.f, -0.4 * (magMoon + 54.32f));
 
        C3 = pow(10.f, -0.4f * K * airMassMoon);// Term for moon brightness computation
 
        bTwilightTerm = -6.724f + 22.918312f * (Constant.PI_OVER_TWO - acos(cosDistSunZenith));
 
        C4 = pow(10.f, -0.4f * K * airMassSun);// Term for sky brightness computation
    }
 
    /**
     * Compute the luminance at the given position
     *
     * @param cosDistMoon cos(angular distance between moon and the position)
     * @param cosDistSun  cos(angular distance between sun  and the position)
     * @param cosDistZenithcos(angulardistancebetweenzenithandtheposition)
     *
     * @return
     */
    public double getLuminance(double cosDistMoon, double cosDistSun, double cosDistZenith) {
        // catch rounding errors here or end up with white flashes in some cases
        if (cosDistMoon < -1.d) cosDistMoon = -1.d;
        if (cosDistMoon > 1.d) cosDistMoon = 1.d;
        if (cosDistSun < -1.d) cosDistSun = -1.d;
        if (cosDistSun > 1.d) cosDistSun = 1.d;
        if (cosDistZenith < -1.d) cosDistZenith = -1.d;
        if (cosDistZenith > 1.d) cosDistZenith = 1.d;
 
        double distMoon = FastMath.acos(cosDistMoon);
        double distSun = FastMath.acos(cosDistSun);
 
        // Air mass
        double X = 1.d / (cosDistZenith + 0.025f * FastMath.exp(-11.d * cosDistZenith));
        double bKX = pow(10.d, -0.4f * K * X);
 
        // Dark night sky brightness
        bNight = 0.4f + 0.6f / FastMath.sqrt(0.04f + 0.96f * cosDistZenith * cosDistZenith);
        bNight *= bNightTerm * bKX;
 
        // Moonlight brightness
        double FM = 18886.28 / (distMoon * distMoon + 0.0007f) + pow(10.d, 6.15f - (distMoon + 0.001) * 1.43239f);
        FM += 229086.77f * (1.06f + cosDistMoon * cosDistMoon);
        bMoon = bMoonTerm1 * (1.d - bKX) * (FM * C3 + 440000.d * (1.d - C3));
 
        //Twilight brightness
        bTwilight = pow(10.d, bTwilightTerm + 0.063661977f * FastMath.acos(cosDistZenith) / K) *
                (1.7453293f / distSun) * (1.d - bKX);
 
        // Daylight brightness
        double FS = 18886.28f / (distSun * distSun + 0.0007f) + pow(10.d, 6.15f - (distSun + 0.001) * 1.43239f);
        FS += 229086.77f * (1.06f + cosDistSun * cosDistSun);
        bDaylight = 9.289663e-12 * (1.d - bKX) * (FS * C4 + 440000.d * (1.d - C4));
 
        // 27/08/2003 : Decide increase moonlight for more halo effect...
        bMoon *= 2.;
 
        // Total sky brightness
        bTotal = bDaylight > bTwilight ? bNight + bTwilight + bMoon : bNight + bDaylight + bMoon;
 
        return (bTotal < 0.d) ? 0.d : bTotal * 900900.9f * PI * 1e-4 * 3239389 * 2;
        //5;	// In cd/m^2 : the 32393895 is empirical term because the
        // lambert -> cd/m^2 formula seems to be wrong...
    }
 
    /*
250 REM  Visual limiting magnitude
260 BL=B(3)/1.11E-15 : REM in nanolamberts*/
 
    // Airmass for each component
    //cos_dist_zenith =cos(dist_zenith);
    //double gaz_mass = 1.f / ( cos_dist_zenith + 0.0286f *exp(-10.5f * cos_dist_zenith) );
    //double aerosol_mass = 1.f / ( cos_dist_zenith + 0.0123f *exp(-24.5f * cos_dist_zenith) );
    //double ozone_mass = 1.f /sqrt( 0.0062421903f - cos_dist_zenith * cos_dist_zenith / 1.0062814f );
    // Total extinction for V band
    //double DM = KR*gaz_mass + KA*aerosol_mass + KO*ozone_mass + KW*gaz_mass;
 
    /*
	// Visual limiting magnitude
	if (BL>1500.0)
	{
		C1 = 4.466825e-9;
		C2 = 1.258925e-6;
	}
	else
	{
		C1 = 1.584893e-10;
		C2 = 0.012589254;
	}
 
	double TH = C1*Math.pow(1.f+Math.sqrt(C2*BL),2.f); // in foot-candles
	double MN = -16.57-2.5*Math.log10(TH)-DM+5.0*Math.log10(SN); // Visual Limiting Magnitude
	*/
 
    /**
     * Air mass for the Moon
     */
    private double airMassMoon;
 
    /**
     * Air mass for the Sun
     */
    double airMassSun;
 
    /**
     * Total brightness
     */
    double bTotal;
 
    /**
     * Dark night brightness
     */
    double bNight;
 
    /**
     * Twilight brightness
     */
    double bTwilight;
 
    /**
     * Daylight sky brightness
     */
    double bDaylight;
 
    /**
     * Moon brightness
     */
    double bMoon;
 
    /**
     * Moon magnitude
     */
    double magMoon;
 
    /**
     * Something related with date
     */
    double RA;
 
    /**
     * Useful coef...
     */
    double K;
 
    /**
     * Term for moon brightness computation
     */
    double C3;
 
    /**
     * Term for sky brightness computation
     */
    double C4;
 
    /**
     * Snellen Ratio (20/20=1.0, good 20/10=2.0)
     */
    double SN = 1;
 
    // Optimisation variables
    double bNightTerm;
 
    double bMoonTerm1;
 
    double bTwilightTerm;
}

The method getSkyBrightness contains some variables that can be adapted in case you want to exagerate the brightness (atmBr, atmI) or the red glow close to the horizon (turbidity). I have set them in a fast way after a few tests to try to use acceptable values depending on the Sun elevation.

These kind of images are already being used for showing the sky during the day at the web page of the OAN ephemerides server.

2018/02/19 16:10 · Tomás Alonso Albi · 1 Comment

Using GPhoto for move detection

This is my first blog entry in more than two years. I've been very busy with my Android planetarium project, now almost finished, but probably needed also some rest. Despite of that during this time I have developed a number of little nice projects, and waited for the right moment to resume my blog activity and release some of them. As ussual they are not oriented to specific goals, going from 3d renderings to solving complex and time consuming tasks in a completely automatic manner, or even to criptocurrencies.

In this post I present a little program aimed to detecting move and triggering shots with a camera automatically. It is based on the gphoto library, and uses the gphoto binding implemented in the JPARSEC library. Some features of the binding have been improved or developed to support this use case. For instance, the live view mode didn't support executing other commands except those to capture previews, and it wasn't possible to execute this mode without showing a panel object with those previews.

The program provides many (31) configuration options in a file named config.txt. Among others, it is possible to set some custom parameters for the camera at startup (ISO, shutter, aperture, among others) and right before the program ends, adjust the sensitivity of the move detection algorithm, configure masks the search for move only in certain areas, or to uppload the shots to a server.

My intenction is to use this program for security, shooting when move is detected with a high resolution camera instead of a webcam. After thinking from time to time on this a saw a post in the gphoto mail list from Alan Corey, asking for a way to do something similar for wild photography. So I decided to start writing the program and Alan collaborated testing it in his cameras. In fact, the program has been tested successfully in a Canon 40D, a Nikon D5200, and a Canon Powershot S70 and A520. It has also been tested in a variety of CPU devices, from a powerful desktop to a Raspberry PI 3. Here I will describe the basics of this program, some other details can be found in the gphoto mailing list and previous messages about the same subject.

The move detection algorithm is very simple. It searches for move in two ways: detecting a global change in the luminosity of the preview image, and detecting changes in the brightness level of the pixels. The preview image is internally converted to gray scale since the color data is not required to detect move. The global change is computed from the difference between the histograms of the preview image and a reference preview image taken when the program starts. This reference image should be free of movement, and is optionally updated in a given time interval to account for possible brightness changes due to the sky or the weather. The histogram difference is computed pixel by pixel, and this change in the gray color is also used to compute the number of individual pixels that present some move or change in that level, according to a different change criteria specific to individual pixels. So the second move detection check is computed from the percentage of individual pixels showing move, and the move is considered as detected when any of those two methods trigger a detection.

When this happens the program pauses the live view in the JPARSEC binding to execute other commands to take a number of shots, resuming back after that the live view mode.

The program has as dependency the jsch library to uppload images to a server, and the itextpdf for its Base64 class used here to include images in the web server. The idea is to compile it with Java 6 at least. Here is the code of the main class.

GPhotoMoveDetection.java
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
 
import com.itextpdf.text.pdf.codec.Base64;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
 
import jparsec.graph.DataSet;
import jparsec.graph.chartRendering.AWTGraphics;
import jparsec.io.ConsoleReport;
import jparsec.io.FileIO;
import jparsec.io.ReadFile;
import jparsec.io.device.implementation.GPhotoCamera;
import jparsec.io.device.implementation.GPhotoCamera.CAMERA_ID;
import jparsec.io.device.implementation.GPhotoCamera.CAMERA_PARAMETER;
import jparsec.io.image.Picture;
import jparsec.time.AstroDate;
import jparsec.vo.FTP;
 
/**
 * An example of using GPhoto library (through the binding integrated in JPARSEC) to control 
 * a DSLR camera and trigger a photo when movement is detected.
 * 
 * @author T. Alonso Albi - OAN (Spain)
 * @version 1.0
 */
public class GPhotoMoveDetection {
 
	// Export only this class to gphoto.jar and execute with:
	// java -classpath gphoto.jar:jparsec.jar:jsch-0.1.41.jar:jsch-0.1.41.jar:itextpdf-5.1.3.jar research.other.GPhotoMoveDetection 
 
	// TODO:
	// Option to pause motion detection ?
	// Server page reloading all the time, problematic with high fps. Reload by sections with different rates ?
	// Is it possible to trigger several shots quickly from shell ? Option to use the other method ?
	// Avoid overwriting old shots ?
	// Send e-mail with move alert ?
 
	public static GPhotoCamera c = null;
	public static String inISO, inAPERTURE, inCAPTURE_TARGET, inNIKON_QUALITY, inRESOLUTION, inSHUTTER_SPEED;
	public static String outISO, outAPERTURE, outCAPTURE_TARGET, outNIKON_QUALITY, outRESOLUTION, outSHUTTER_SPEED;
	public static FTP ftp = null;
	public static ArrayList<String> messages = new ArrayList<String>(), shotList = new ArrayList<String>();
	public static ArrayList<Object> thumbs = new ArrayList<Object>();
	public static int n = 0, nn = 0, moveEvents = 0;
	public static long lastMoveEvent;
	public static boolean forceExit = false, forcePause = false, createThumbs = true, keepInCamera = false;
	public static int webServerPort = 8080; // Server port (int)
	public static int liveMaxTime = 0; // sec, <= 0 => for ever
	public static int updateRef = 3600; // sec, -1 => never update
	public static int resample = 320; // width, -1 => no resample
	public static int thresholdBr = 10; // brightness % change to trigger global movement 
	public static int thresholdPx = 10; // brightness % change to trigger pixel movement 
	public static int movingPixPerc = 5; // % of pixel with change greater than threshold to trigger local movement
	public static int fps = -2; // fps of live view, < 0 for <1 fps (-2 => 0.5 fps)
	public static int nshot = 3; // number of shots to take
	public static int maxShot = 1000; // maximum number of shots to take
	public static boolean rename = true; // True to rename shots to 'shot_xxx.jpg'
	public static String sound = null;
	public static String server = null, user = null, pass = null, remoteDir = null; // to uppload images
	public static String extension = ".jpg"; // extension in lowercase for the new relevant images
	public static String mask[] = new String[] {
		"--------------------------------",
		"--------------------------------",
		"--------------------------------",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"++++++++++++++++++++++++++++++++",
		"--------------------------------",
		"--------------------------------",
		"--------------------------------",
	};
	public static boolean debug = true;
 
	/**
	 * Test program.
	 * @param args Unused.
	 */
	public static void main(String args[])
	{
		try {			
			String dir = FileIO.getWorkingDirectory();
			loadConfig(false);
 
			String out[] = GPhotoCamera.autoDetect();
			if (debug) System.out.println("Using gphoto "+GPhotoCamera.gphotoVersion);
			if (debug) System.out.println("Detected cameras:");
			ConsoleReport.stringArrayReport(out);
			c = new GPhotoCamera(CAMERA_ID.EOS40D, null, dir, false, debug);
 
			// Set custom camera parameters or read them from camera
			inISO = setParameter(inISO, CAMERA_PARAMETER.ISO);
			inAPERTURE = setParameter(inAPERTURE, CAMERA_PARAMETER.APERTURE);
			inCAPTURE_TARGET = setParameter(inCAPTURE_TARGET, CAMERA_PARAMETER.CAPTURE_TARGET);
			inNIKON_QUALITY = setParameter(inNIKON_QUALITY, CAMERA_PARAMETER.NIKON_QUALITY);
			inRESOLUTION = setParameter(inRESOLUTION, CAMERA_PARAMETER.RESOLUTION);
			inSHUTTER_SPEED = setParameter(inSHUTTER_SPEED, CAMERA_PARAMETER.SHUTTER_SPEED);
 
			c.setTimeLimitForLiveView(liveMaxTime);
			c.setLiveFPS(fps);
			c.setCopyInCamera(keepInCamera);
 
			if (debug) {
				CAMERA_PARAMETER cv[] = CAMERA_PARAMETER.values();
				for (int i=0; i<cv.length; i++)
				{
					System.out.println("Possible values of "+cv[i]);
					String values[] = c.getConfig(cv[i]);
					ConsoleReport.stringArrayReport(values);
				}
			}
 
			try {
				if (debug) System.out.println("Creating web server on port "+webServerPort);
				createServer(webServerPort);
			} catch (Exception e) {
				e.printStackTrace();
			}
 
			n = 0;
			nn = 0;
			long lastShotTime = -1;
			while(!forceExit) {
				if (forcePause) {
					if (debug) System.out.println("Live view is currently paused");					
				} else {
					if (c.isLivePaused()) {
						if (debug) System.out.println("Resuming live view");
						c.resumeLiveView();
						loadConfig(true);
					} else {
						if (debug) System.out.println("Starting live view");
						if (!c.isLive()) c.startLiveView(null, fps);
					}
				}
				long wait = (1000 / fps);
				if (fps < 0) wait = (long) (1000 / (1.0 / Math.abs(fps)));
				while (!forcePause) {
					try {
						if (forceExit) break;
						if (!c.isLive()) {
							if (debug) System.out.println("Detected live view was stopped (limit time). Waiting 5s and restarting live view mode ...");
							long t0 = System.currentTimeMillis();
							long t1 = t0 + 5000; // Wait 5s to restart live view
							while (true) {
								long t2 = System.currentTimeMillis();
								if (t2 < t1) {
									Thread.sleep(500);
									continue;
								}
								break;
							}
							String lastShot = c.getLastShotPath();
							if (lastShot != null && FileIO.exists(lastShot)) FileIO.deleteFile(lastShot);
							if (debug) System.out.println("Starting live view");
							c.startLiveView(null, fps);
						}
 
						String lastShot = c.getLastShotPath();
						boolean move = false;
						if (lastShot != null && FileIO.exists(lastShot)) {
							long time = (new File(lastShot)).lastModified();
							if (time != lastShotTime) {
								Picture pic = new Picture(lastShot);
								move = detectMove(pic);
							}
							lastShotTime = time;
						}
 
						if (move) {
							if (debug) System.out.println("Pausing live view");
							c.pauseLiveView();
							long t0 = System.currentTimeMillis();
							long t1 = t0 + wait * 2;
							while (true) {
								long t2 = System.currentTimeMillis();
								if (t2 < t1) {
									Thread.sleep(100);
									continue;
								}
								break;
							}
							break;
						}
 
						Thread.sleep(wait / 50);
					} catch (Exception exc) {
						exc.printStackTrace();
						continue;
					}
				}
 
				if (forceExit) break;
				if (forcePause) {
					if (!c.isLivePaused()) c.pauseLiveView();
					long t0 = System.currentTimeMillis();
					long t1 = t0 + wait * 2;
					while (true) {
						long t2 = System.currentTimeMillis();
						if (t2 < t1) {
							try {
								Thread.sleep(1000);
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							continue;
						}
						break;
					}
					continue;
				}
 
				// Shot/s after move detection
				moveEvents ++;
				lastMoveEvent = System.currentTimeMillis();
				addMessage("Move detected!");
				if (n > maxShot) {
					addMessage("Aborting taking shots: too much files (> "+maxShot+")");
				} else {
					// Shot inside shell using the binding
					String com = "capture-image-and-download";
					boolean lock = false;
 
					String path = "";
					for (int i=0; i<nshot; i++) {
						c.executeExternalCommandInPausedLiveView(com);
						long t0 = System.currentTimeMillis();
						while (true) {
							Thread.sleep(50);
							if (c.getExternalCommandInPausedLiveView() == null) break;
							if (System.currentTimeMillis() - t0 > 30000) {
								lock = true;
								break;
							}
						}
						if (lock) break;
						path += c.getLastShotPath()+",";
					}
					if (!path.equals("")) path = path.substring(0, path.length()-1);
 
					if (lock) addMessage("Detected camera freeze when taking shots (too much of them ?)");
					if (debug) System.out.println("Created files: "+path);
					n += nshot;
					if (!lock && !path.equals("")) {
						String p[] = DataSet.toStringArray(path, ",");
						for (int i=0; i<p.length; i++) {
							if (p[i].toLowerCase().endsWith(extension)) {
								if (debug) System.out.println("New shot found: "+p[i]);
								Picture pic = null;
								if (rename) {
									nn ++;
									pic = new Picture(p[i]);
									pic.write("shot"+nn+".jpg");
									FileIO.deleteFile(p[i]);
									p[i] = "shot"+nn+".jpg";
								}								
								if (createThumbs) {
									shotList.add(p[i]);
									if (pic == null) pic = new Picture(p[i]);
									if (resample > 0) pic.getScaledInstance(resample, 0, true);
									thumbs.add(pic.getImage());
								}
								if (ftp != null) {
									if (debug) System.out.println("Upploading "+p[i]+" to "+user+"@"+server);
									ftp.uppload(p[i], p[i]);
								}
							}
						}
					}
				}
 
				loadConfig(true);
				c.resumeLiveView();
 
				try {
					Thread.sleep(wait*2);
				} catch (Exception exc) {
					exc.printStackTrace();
				}
			}
 
			// Set at the end custom camera parameters or those read from camera at the beginning
			setParameter(inISO, outISO, CAMERA_PARAMETER.ISO);
			setParameter(inAPERTURE, outAPERTURE, CAMERA_PARAMETER.APERTURE);
			setParameter(inCAPTURE_TARGET, outCAPTURE_TARGET, CAMERA_PARAMETER.CAPTURE_TARGET);
			setParameter(inNIKON_QUALITY, outNIKON_QUALITY, CAMERA_PARAMETER.NIKON_QUALITY);
			setParameter(inRESOLUTION, outRESOLUTION, CAMERA_PARAMETER.RESOLUTION);
			setParameter(inSHUTTER_SPEED, outSHUTTER_SPEED, CAMERA_PARAMETER.SHUTTER_SPEED);
 
			if (debug) System.out.println("Exiting ...");
			if (c.isLivePaused()) c.resumeLiveView();
			c.stopLiveView();
			try {
				long wait = (1000 / fps);
				if (fps < 0) wait = (long) (1000 / (1.0 / Math.abs(fps)));
				long t0 = System.currentTimeMillis();
				long t1 = t0 + wait * 3;
				while (true) {
					long t2 = System.currentTimeMillis();
					if (t2 < t1) {
						try {
							Thread.sleep(100);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						continue;
					}
					break;
				}
 
				System.exit(0);
			} catch (Exception exc) {
				exc.printStackTrace();
			}
 
		} catch (Exception exc)
		{
			exc.printStackTrace();
		}
	}
 
	public static String setParameter(String value, CAMERA_PARAMETER p) throws Exception {
		if (c == null || value == null) return value;
		if (value.equals("null")) {
			value = c.getParameterFromCamera(p);
		} else {
			c.setParameter(p, value);
		}
		return value;
	}
 
	public static void setParameter(String valueIn, String valueOut, CAMERA_PARAMETER p) throws Exception {
		if (c == null || valueIn == null || valueOut == null) return;
		if (valueOut.equals("null")) {
			c.setParameter(p, valueIn);
		} else {
			c.setParameter(p, valueOut);
		}
	}
 
	public static void loadConfig(boolean reload) throws Exception {
		String dir = FileIO.getWorkingDirectory(), config = dir + "config.txt";
		if (!FileIO.exists(config) && !reload) {
			System.out.println("Cannot find config file "+config);
			System.exit(0);
		}
 
		String d[] = DataSet.arrayListToStringArray(ReadFile.readAnyExternalFile(config));
		String param[] = new String[] {
			"UPDATE_REF", "RESAMPLE", "THRESHOLD_BR", "THRESHOLD_PX", "MOVING_PIX_PER", "FPS", 
			"NSHOTS", "MAX_SHOTS", "EXTENSION", "MASK", "RENAME", "SERVER", "USER", "PASSW", 
			"REMOTE_DIR", "LIVE_MAX_TIME", "WEB_SERVER_PORT", "CREATE_THUMBS", "KEEP_IN_CAMERA",
			"ISO_IN", "APERTURE_IN", "CAPTURE_TARGET_IN", "NIKON_QUALITY_IN", "RESOLUTION_IN", 
			"SHUTTER_SPEED_IN", "ISO_OUT", "APERTURE_OUT", "CAPTURE_TARGET_OUT", "NIKON_QUALITY_OUT", 
			"RESOLUTION_OUT", "SHUTTER_SPEED_OUT"
 
		};
		for (int i=0; i<d.length; i++) {
			for (int j=0; j<param.length; j++) {
				if (d[i].startsWith(param[j])) {
					if (debug) System.out.println("Reading "+d[i]);
					String val = d[i].substring(d[i].indexOf(" ")).trim();
					if (val.indexOf("//") > 0) val = val.substring(0, val.indexOf("//")).trim();
 
					if (j == 0) updateRef = Integer.parseInt(val);
					if (j == 1) resample = Integer.parseInt(val);
					if (j == 2) thresholdBr = Integer.parseInt(val);
					if (j == 3) thresholdPx = Integer.parseInt(val);
					if (j == 4) movingPixPerc = Integer.parseInt(val);
					if (j == 5) fps = Integer.parseInt(val);
					if (j == 6) nshot = Integer.parseInt(val);
					if (j == 7) maxShot = Integer.parseInt(val);
					if (j == 8) extension = val;
					if (j == 9) mask = DataSet.getSubArray(d, i+1, d.length-1);
					if (j == 10) rename = Boolean.parseBoolean(val);
					if (j == 11) server = val;
					if (j == 12) user = val;
					if (j == 13) pass = val;
					if (j == 14) remoteDir = val;
					if (j == 15) liveMaxTime = Integer.parseInt(val);
					if (j == 16) webServerPort = Integer.parseInt(val);
					if (j == 17) createThumbs = Boolean.parseBoolean(val);
					if (j == 18) keepInCamera = Boolean.parseBoolean(val);
 
					if (reload) continue;
					if (j == 19) inISO = val;
					if (j == 20) inAPERTURE = val;
					if (j == 21) inCAPTURE_TARGET = val;
					if (j == 22) inNIKON_QUALITY = val;
					if (j == 23) inRESOLUTION = val;
					if (j == 24) inSHUTTER_SPEED = val;
					if (j == 25) outISO = val;
					if (j == 26) outAPERTURE = val;
					if (j == 27) outCAPTURE_TARGET = val;
					if (j == 28) outNIKON_QUALITY = val;
					if (j == 29) outRESOLUTION = val;
					if (j == 30) outSHUTTER_SPEED = val;
				}
			}
		}
 
		if (ftp != null) ftp.disconnect();
		ftp = null;
		if (server != null && !server.equals("null") && user != null && !user.equals("null") 
				&& pass != null && !pass.equals("null")) 
			ftp = new FTP(server, user, pass);
		if (remoteDir != null && !remoteDir.equals("null")) ftp.changeDirectory(remoteDir);
 
		if (reload) addMessage("Configuration reloaded");
		if (c != null) {
			c.setLiveFPS(fps);
			c.setTimeLimitForLiveView(liveMaxTime);
			c.setCopyInCamera(keepInCamera);
		}
 
		String pp = dir + "sound.mp3";
		sound = null;
		if (FileIO.exists(pp)) {
			byte[] bytes = Files.readAllBytes(Paths.get(pp));
			sound = Base64.encodeBytes(bytes); 
			sound = "data:audio/mp3;base64,"+sound;
		}
	}
 
	public static Picture ref = null, lastPicColor = null, refColor = null;
	public static long lastRef = 0;
	public static boolean detectMove(Picture pic) throws Exception {
		if (resample > 0) pic.getScaledInstance(resample, 0, true);
 
		// Set reference at startup
		BufferedImage copy = Picture.copy(pic.getImage());
		if (refColor == null) {
			refColor = new Picture(copy);
			Graphics2D g2 = refColor.getImage().createGraphics();
			AWTGraphics.enableAntialiasing(g2);
			float fs = 20;
			g2.setColor(Color.RED);
			g2.setFont(g2.getFont().deriveFont(fs));
			g2.drawString(addMessage(null), fs, refColor.getHeight()-fs/2);
			g2.setColor(Color.WHITE);
			for (int y=0; y<pic.getHeight(); y++) {
				int cy = (int) (0.5 + (mask.length - 1.0) * (y / (pic.getHeight() - 1.0)));
				for (int x=0; x<pic.getWidth(); x++) {
					int cx = (int) (0.5 + (mask[cy].length() - 1.0) * (x / (pic.getWidth() - 1.0)));
					if (mask[cy].substring(cx, cx+1).equals("-"))
						continue;
					if (x % 3 != 0 || y % 3 != 0) continue;
					g2.drawLine(x-1, y, x+1, y);
					g2.drawLine(x, y+1, x, y-1);
				}
			}
			g2.dispose();
 
			pic.toGrayScale();
			ref = pic;
			lastRef = System.currentTimeMillis();
			addMessage("Reference created");
			return false;
		}
 
		// Detect movement
		lastPicColor = new Picture(pic.getImage());
		Graphics2D g2 = lastPicColor.getImage().createGraphics();
		AWTGraphics.enableAntialiasing(g2);
		float fs = 20;
		g2.setColor(Color.RED);
		g2.setFont(g2.getFont().deriveFont(fs));
		g2.drawString(addMessage(null), fs, refColor.getHeight()-fs/2);
		g2.setColor(Color.WHITE);
 
		pic.toGrayScale();
		boolean move = false;
		double histo[] = new double[255];
		int movingPixels = 0, maskedPixels = 0;		
		for (int y=0; y<pic.getHeight(); y++) {
			int cy = (int) (0.5 + (mask.length - 1.0) * (y / (pic.getHeight() - 1.0)));
			for (int x=0; x<pic.getWidth(); x++) {
				int cx = (int) (0.5 + (mask[cy].length() - 1.0) * (x / (pic.getWidth() - 1.0)));
				if (mask[cy].substring(cx, cx+1).equals("-")) {
					maskedPixels ++;
					continue;
				}
 
				Color c1 = pic.getColorAt(x, y);
				Color c2 = ref.getColorAt(x, y);
				int gray1 = c1.getRGB() & 255;
				int gray2 = c2.getRGB() & 255;
				int g = Math.abs(gray1 - gray2);
				histo[g] ++;
				if (g > thresholdPx*2.55) {
					movingPixels ++;
					if (x % 3 != 0 || y % 3 != 0) continue;
					g2.drawLine(x-1, y, x+1, y);
					g2.drawLine(x, y+1, x, y-1);
				}
			}			
		}
		g2.dispose();
		int maxIndex = (int) (DataSet.getIndexOfMaximum(histo) / 2.55);
		double percMov = movingPixels * 100.0 / (double) (pic.getWidth() * pic.getHeight() - maskedPixels);
		if (maxIndex > thresholdBr || percMov > movingPixPerc) move = true;
		if (debug) System.out.println("Global move: "+maxIndex+"/"+thresholdBr+". Local move: "+(float)percMov+"/"+movingPixPerc);
 
		// Update reference if required
		double elapsed = (System.currentTimeMillis() - lastRef) * 0.001;
		if (!move && updateRef > 0 && elapsed > updateRef) {
			ref = pic;
			refColor = new Picture(copy);
			lastRef = System.currentTimeMillis();			
 
			g2 = refColor.getImage().createGraphics();
			AWTGraphics.enableAntialiasing(g2);
			fs = 20;
			g2.setColor(Color.RED);
			g2.setFont(g2.getFont().deriveFont(fs));
			g2.drawString(addMessage(null), fs, refColor.getHeight()-fs/2);
			g2.setColor(Color.WHITE);
			for (int y=0; y<pic.getHeight(); y++) {
				int cy = (int) (0.5 + (mask.length - 1.0) * (y / (pic.getHeight() - 1.0)));
				for (int x=0; x<pic.getWidth(); x++) {
					int cx = (int) (0.5 + (mask[cy].length() - 1.0) * (x / (pic.getWidth() - 1.0)));
					if (mask[cy].substring(cx, cx+1).equals("-"))
						continue;
					if (x % 3 != 0 || y % 3 != 0) continue;
					g2.drawLine(x-1, y, x+1, y);
					g2.drawLine(x, y+1, x, y-1);
				}
			}
 
			addMessage("Reference updated");
		}
		return move;
	}
 
	public static String addMessage(String msg) {
		AstroDate astro = new AstroDate();
		String m = astro.toString();
		if (msg != null) {
			m += ": "+msg;
			messages.add(0, m);
 
			if (debug) System.out.println(msg);
		}
		return m;
	}
 
	public static String commands[] = new String[] {
		"Pause,left,Pausing web server",
		"Resume,center,Resuming web server",
		"Exit,right,Exiting"
	};
	public static String imgFormat = "jpg"; // jpg to reduce file size
	public static void createServer(int port) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
        server.createContext("/", new MyHandler(-1));
 
        for (int i=0; i<commands.length; i++) {
        	String name = "com"+i;
 
            server.createContext("/"+name, new MyHandler(i));
        }
 
        server.setExecutor(null); // creates a default executor
        server.start();
	}
 
   static class MyHandler implements HttpHandler {
    	int index = -1;
 
    	public MyHandler() {
		}
 
    	public MyHandler(int i) {
    		this.index = i;
		}
 
        @Override
        public void handle(HttpExchange t) throws IOException {
        	StringBuffer response = new StringBuffer();
        	String sep = "<BR><BR>", step = FileIO.getLineSeparator();
 
			long wait = (1000 / fps);
			if (fps < 0) wait = (long) (1000 / (1.0 / Math.abs(fps)));
			int sec = (int) (wait / 1000 + 0.5);
    		response.append("<html><head><title>Web server</title><meta http-equiv=\"refresh\" content=\""+sec+", url=/\"></head><body bgcolor=\"#000000\">");
    		response.append("<center><H1 style=\"color: white\">GPhoto web server</H1></center>"+ step);
    		try {
    			response.append("<p style=\"color: white; font-size:30px; float: left\">Reference</p><p style=\"color: white; font-size:30px; float: right\">Live view (last image)</p>" + sep + step);
    			response.append("<div style=\"display: inline-block\">" + step);
        		if (refColor != null)
        			response.append("<img src=\""+refColor.imageToString(imgFormat, true)+"\" style=\"width: 49%; float: left\" />" + step);
        		if (lastPicColor != null)
        			response.append("<img src=\""+lastPicColor.imageToString(imgFormat, true)+"\" style=\"width: 49%; float: right\" />"+step);
        		response.append("</div>" + step);
    		} catch (Exception exc) {
    			exc.printStackTrace();
    		}
    		response.append(sep + "<HR>" + step);
    		if (c != null) {
    			response.append("<p style=\"color: white; font-size:20px\">Camera: "+c.getModel()+", port "+c.getPort()+"</p>"+step);
    			response.append("<p style=\"color: white; font-size:20px\">Working directory: "+c.getWorkingDirectory()+"</p>"+step);
        		String status = "running";
        		if (c.isLivePaused()) status = "paused";
    			response.append("<p style=\"color: white; font-size:20px\">Status: "+status+"</p>"+step);
    			response.append("<p style=\"color: white; font-size:20px\">Move events: "+moveEvents+"</p>"+step);
    			response.append("<p style=\"color: white; font-size:20px\">Shots taken: "+n+"</p>"+step);
    			String config = "liveMaxTime="+liveMaxTime+", ";
    			config += "updateRef="+updateRef+", ";
    			config += "resample="+resample+", ";
    			config += "thresholdBr="+thresholdBr+", ";
    			config += "thresholdPx="+thresholdPx+", ";
    			config += "movingPixPerc="+movingPixPerc+", ";
    			config += "fps="+fps+", ";
    			config += "nshot="+nshot+", ";
    			config += "maxShot="+maxShot+", ";
    			config += "rename="+rename+", ";
    			config += "createThumbs="+createThumbs+", ";
    			config += "keepInCamera="+keepInCamera+", ";
    			config += "server="+user+"@"+server+", ";
    			response.append("<p style=\"color: white; font-size:20px\">Configuration: "+config+"</p>"+step);
    		}
 
    		response.append("<HR>" + sep + step);
    		response.append("<div style=\"display: block; text-align: center\">" + step);
    		for (int i=0; i<3; i++) {
    			String title = FileIO.getField(1, commands[i], ",", false);
    			String align = FileIO.getField(2, commands[i], ",", false);
    			response.append("<input type=\"button\" style=\"color: white; float: "+align+"; font-size:40px\" onclick=\"location.href='/com"+i+"';\" value=\""+title+"\" />" + step);
    		}
    		response.append("</div>" + step);
 
    		if (index >= 0) {
    			String msg = FileIO.getRestAfterField(2, commands[index], ",", false).trim();
        		response.append("<center><H2 style=\"color: white\">" + msg + sep + step);
        		response.append("</H2></center>");
 
        		if (index == 0) {
        			forcePause = true;
        			addMessage("Paused");
        		}
        		if (index == 1) {
        			forcePause = false;
        			addMessage("Resumed");
        		}
        		if (index == 2) {
        			forceExit = true;
        			addMessage("Exited");
        		}
    		}
 
    		if (shotList != null && shotList.size() > 0) {
    			response.append("<HR>" + step);
 
    			try {
        			boolean left = true;
	        		for (int i=shotList.size()-1; i>=0; i--) {
	        			if (left) response.append("<div style=\"display: inline-block\">" + step);		        			
	        			Picture pic = new Picture((BufferedImage) thumbs.get(i));
	        			response.append("<img src=\""+pic.imageToString(imgFormat, true)+"\" style=\"width: 49%; float: "+(left ? "left" : "right")+"\" />" + step);
	        			if (!left) {
	        				response.append("</div>" + step);
	            			response.append("<p style=\"color: white; font-size:24px; float: left\">"+shotList.get(i+1)+"</p><p style=\"color: white; font-size:24px; float: right\">"+shotList.get(i)+"</p>" + step);
	        			} else {
	        				if (i == 0)
		            			response.append("<p style=\"color: white; font-size:24px; float: left\">"+shotList.get(i)+"</p>" + step);
	        			}
	        			left = !left;
	        		}
    			} catch (Exception exc) {
    				exc.printStackTrace();
    			}
 
    			response.append(sep + sep + sep + sep + sep);
    		}  
 
    		if (messages != null && messages.size() > 0) {
        		response.append("<HR>" + step);
 
        		String m[] = DataSet.arrayListToStringArray(messages);
        		for (int i=0; i<m.length; i++) {
        			response.append("<p style=\"color: white\"; font-size:16px>"+m[i]+"</p>" + step);
        		}
    		}
 
    		if (sound != null && lastMoveEvent > 0) {
    			double elapsed = (System.currentTimeMillis() - lastMoveEvent) * 0.001;
    			if (elapsed < sec) {
		    		response.append("<audio controls=\"controls\" autobuffer=\"autobuffer\" autoplay=\"autoplay\">"+step);
		    		response.append("<source src=\""+sound+"\" />"+step);
		    		response.append("</audio>"+step);
    			}
    		}
 
    		response.append("</body></html>");
 
            t.sendResponseHeaders(200, response.length());
            OutputStream os = t.getResponseBody();
            os.write(response.toString().getBytes());
            os.close();
        }
    }
}

The program includes a little web server to see the reference image with the move detection mask, the latest live view image showing the pixels with movement, some buttons to pause or stop the program, and some log messages. The server also shows the shots taken when move was detected. The next image shows how the web server looks like.


 

To access the web server in the same computer in which gphoto is running and the camera is connected you have to open a browser and load http://localhost:port, where port is the port number configured in the program (8080 by default). Of course knowing the computer IP you can load it from another device in the same network, or even from another place outside that network with a proper configuration of the router.

The full working program can be downloaded from this url. It should work with no modification in Linux and Mac. In Windows the launching script would need some modification (mainly change ; by : and write it to a .bat file), but anyway you first need to compile and install the gphoto library, so anyone capable of doing that would have no problem executing the program.

2018/01/19 13:32 · Tomás Alonso Albi · 0 Comments

ClearSky for Android released

After 4 years developing from time to time a planetarium for Android (since the first experiments with this platform back in 2012), I have published the ClearSky planetarium in two versions: a free version quite generous in features and a paid version which is oriented to become useful to amateur astronomers with telescopes. This second page contains a detailed list of features of the commercial version, although the help document included in ClearSky describes everything with even more detail.

The free version contains almost everything a casual observer would need, including the possibility of showing comets and asteroids, which is usually offered only in commercial programs in Android. But the main difference from other tools is the great accuracy in JPARSEC, superior to most free and commercial programs available even on PC platform. In addition, Spanish and English are supported, with absolutely no adds.


 

Design and user interface

One of my main concerns when developing ClearSky has been to be as objective as possible, so I haven't taken any other Android planetarium as reference to think about the design or the features. After installing all other free planetariums I think most of them are not really helpful for an amateur astronomer, and even some commercial ones (I admit I haven't paid for any of them, I prefer to enjoy developing my own one) seems to have just 'more options' or objects in the paid version, instead of being focused to 'more activities' like observing with telescopes or planning observations with a list of astronomical events or objects. Most of the development comes from JPARSEC, even in terms of design of the different color squemes, and I have developed this library for years, so this Android planetarium has the benefit of years of experience with many little cosmetic and usability improvements. However, Android development is hard, you have to hit your head against hundreds of walls and overtake all problems until you end up with a finished product.

My point of view for an adequate planetarium for general public is a program that must be extremely easy and confortable to use. I have seen too many planetariums with great graphics, but where it is hard to drag and zoom the sky, and the objects are moving everytime. This seems to be a must for other developers just to aparently mimic the natural sky, but for astronomy you need to take some time to do things and you won't want to have everything moving. Despite this, there are programs reasonably well solved in this sense, for instance the Cosmos Celestron Navigator, but it is still hard to select a body and zoom in/out. In ClearSky the sky is updated in regular intervals, and zoom operations are fast and realiable, a simple click with a finger will identify an object and will show the distance respect the previous body identified, and a double click will center that body without the mess of a menu for just that (which in ClearSky is triggered with a long press offering, among others, the options of details of the object and to track it). An example of less well solved is SkEye, where the zoom and rotation operations will mess everything, making impossible to zoom in a given body. In addition, due to rotation and the use of equatorial positions you don't know where is up, where is the horizon, or the azimuth/elevation direction, and there are too many numbers which are not important for a user. SkEye is surprisingly well valorated despite all this and the fact that I've seen three degrees of error in the position of Jupiter, so I won't call it accurate… ClearSky, although it is not as beautiful as other planetariums (although I think well enough worked in that sense), it has a much more useful user interface, making the program a really useful and confortable tool, not a simple toy to play a few minutes with. For instance, you can directly change from drag to zoom or the opposite all the time (keeping always at least one finger on the screen), something not possible in other planetariums.


 

Another issue for me is to make the user interface reasonably beautiful and confortable to the eye. This means using the adequate number and distribution of options and adequate icons. In the main window the icons have colors, and there are six for completely different tasks. SkEye shows 9!, which are too many for a phone and some of them are related to the same thing, like changing simulation conditions or the aspect, things that are not oriented to 'activities'. In ClearSky you have a search button which is a must, an option to change the color squeme which is just useful because many users expect and will play with that, and a help/trivia/more options icon (it is configurable) on the right corner of the first row (easier to click with the finger). The trivia is a game to play, just funny if you play it for a few minutes from time to time. I think it is important to have at least one configurable option for the user. The second row shows the view mode (live, text, augmented reality, and chart modes), the list of astronomical events, and the configuration option, again easier to click in that position. Since in the free version there are only two modes (live and chart), the first option will simply swap both of them, in the paid version there is a menu inevitably. The astronomical events is another must in my opinion, since a user will want to know what's going on in the sky, and if the full moon or the astronomical twilight will limit the time interval of really dark skies. All configuration options are provided through the configuration option, although the most important of them are provided in the configurable option, when it is configured to show a menu called 'more options'. The number of configurable options are a lot, almost 100, but they are properly categorized and offered in two levels, showing by default less options. The interfaz section has an option to change for a simple to a complete user interface, showing all posible options.


 

Text mode

The text mode is something probably missed in any other Android planetarium. It is only available in the commercial version, since this feature is specific to amateur astronomers, not the general public. It lists all deep sky objects and the main double/variable stars, sorted by name, object type, magnitude, position in the sky, or transit time. Supernovae and novae will also appear if you enable them, but only in case they are visible in the sky also (field of view around 50 degrees or lower). There is an option to set an alarm for the transit time (as in the events obviously), in case you want to observe some bodies in their greatest elevations. You can set an object as reference, so that the azimuth/elevation position (or right ascension/declination in case you prefer equatorial coordinates in the configuration) will appear as offsets respect that reference body. The position column will change to distance, so you can sort objects by their distances to the reference one. Very useful to observe interesting objects close to the one you are currently observing. In case you go to the text mode from the live view mode, the text mode will be also 'live', sorting the objects by default respect their distances to the direction the device points to. If you hold the device adequately on top of the telescope, you can use this feature to convert any telescope into a push-to one. In case the device doesn't point perfectly to the object, there is an align option in this case, replacing the option to set an object as reference. In case you just want a few objects you like you can add objects to a list of favourites and show only them.


 

Astronomical events

The list of astronomical events is useful to prepare an observation night. In addition to the main general events offered in the free version, the paid version includes events related to natural satellites (main Jupiter, but also for Saturn and Uranus). The events for natural satellites will show even mutual events of natural satellites (for instance a moon of Jupiter occulting or eclipsing another moon), something very interesting for amateur astronomers and probably only offered in ClearSky because of its great accuracy. The list of events for artificial satellites will show the next transits of the main satellites (ISS, HST, and Tiangong 1), as well as their transits on top of the solar and lunar disks. Iridium flares are also computed and simulated.


 

Astronomical equipment

The commercial version of ClearSky can show the field of view of any telescope with horizontal or equatorial mounts. When a telescope is selected, a long click on a star will add another option in the context menu allowing to test the polar alignment of that equipment in that star and in that moment. The program asks for four values (see documentation) and will compute from the deviations of the star (measured in pixels in the camera) where the mount is really pointing to, so that an incremental correction to improve the alignment is possible. The feature is not completely in its final status, but works.


 

Other features

The links provided at the top will list most of the features of ClearSky, but I would like to emphasize some of them. First, the catalog of stars, and specially deep sky objects, are very robust. I worked myself on the deep sky catalog for years, fixing and improving things with time and checking coordinates with Simbad (it is based on the revised NGC).

You can reach magnitude 16 in the commercial programs with the only condition of having network connection (previously downloaded fields can be used offline too). Other programs requires 1 GB of star data in your device, which is absurd for a phone. It is something spectacular to resolve globular clusters like M13 in stars, even without deep sky textures, and how star positions and textures match completely.

700 textures of deep sky objects are overlaided on the sky with great accuracy, corrected by precesion and nutation. The commercial version has high resolution textures with the possibility of downloading more.

Visual quality and accuracy are worked to a great level, so that accurate and realistic planetary rendering is possible in ClearSky. It is not as fast as I would like, but good enough (illumination and everything is done pixel by pixel without 3d OpenGL). You can even identify planetary features when clicking with the finger, something in fact used to allow simulating the sky from other bodies.


 

There are also useful color squemes, like these two screenshots show:


 

And if you are curious about the trivia, here are two screenshots of it. There are more than 200 different questions, implemented in Spanish and English, although many of them are related to identifying objects or constellations.


 

Accuracy

I will not add more words about accuracy, I have already talked about that before, comparing JPARSEC with other PC programs. In ClearSky the accuracy means this program is suitable to studies in the field of ancient arqueoastronomy, since proper motions of stars (the natural change in the shape of constellations) are considered, and planets will appear up to year 3000 B.C. Obviously more accuracy means more complicated and slower algorithms, and this is less compatible with showing the sky in real time (with objects moving), specially in a platform with strong memory and speed limitations. I don't like it, but I admit it can be required to track artificial satellites which moves fast. I currently have a list of a few bugs I have to correct, and some features which will be implemented with time to end up with a product of my (and hopefully also others) like. The hard work is done, but this is just the first release.

Gallery

Selected astronomical images taken with my equipment. As you will see, I'm not a professional photographer, but I do my best. Some of them are old images taken with a film camera, recent ones uses a digital SLR. In addition to the images I have also some videos:

Solar eclipse Solar annular eclipse on October, 3, 2005. It was my first day at OAN so I couldn't use my instrumental. I simply took the camera on my hands.

Venus transit: Venus transit on the Sun on July, 8, 2004. The black drop effect is clearly visible.

Solar eclipse: Solar eclipse of August, 11, 1999, as was shot through my S/C 20 cm telescope.

Log of visits

 
blog.txt · created: 2010/01/31 01:56 (Last modified 2014/10/23 12:51) by Tomás Alonso Albi
 
Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki