JPARSEC Sky Atlas
Table of Contents

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;
	}
}

Discussion

Enter your comment
   ___      __   _  __ ______   __ __
  / _ \ __ / /  / |/ //_  __/  / // /
 / // // // /  /    /  / /    / _  / 
/____/ \___/  /_/|_/  /_/    /_//_/
 
 
blog/skyatlasrevised.txt · created: 2019/02/19 18:12 (Last modified 2019/02/24 10:00) 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