/* * Planet.java * * Copyright (C) 2011-2016 MegaMek team * Copyright (c) 2011 Jay Lawson <jaylawson39 at yahoo.com>. All rights reserved. * * This file is part of MekHQ. * * MekHQ 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 3 of the License, or * (at your option) any later version. * * MekHQ 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 MekHQ. If not, see <http://www.gnu.org/licenses/>. */ package mekhq.campaign.universe; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.TreeMap; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import org.joda.time.DateTime; import org.joda.time.DateTimeComparator; import megamek.common.EquipmentType; import megamek.common.PlanetaryConditions; import mekhq.Utilities; import mekhq.adapter.BooleanValueAdapter; import mekhq.adapter.ClimateAdapter; import mekhq.adapter.DateAdapter; import mekhq.adapter.HPGRatingAdapter; import mekhq.adapter.LifeFormAdapter; import mekhq.adapter.SocioIndustrialDataAdapter; import mekhq.adapter.SpectralClassAdapter; import mekhq.adapter.StringListAdapter; /** * This is the start of a planet object that will keep lots of information about * planets that can be displayed on the interstellar map. * * * @author Jay Lawson <jaylawson39 at yahoo.com> */ @XmlRootElement(name="planet") @XmlAccessorType(XmlAccessType.FIELD) public class Planet implements Serializable { private static final long serialVersionUID = -8699502165157515100L; // Star classification data and methods public static final int SPECTRAL_O = 0; public static final int SPECTRAL_B = 1; public static final int SPECTRAL_A = 2; public static final int SPECTRAL_F = 3; public static final int SPECTRAL_G = 4; public static final int SPECTRAL_K = 5; public static final int SPECTRAL_M = 6; public static final int SPECTRAL_L = 7; public static final int SPECTRAL_T = 8; public static final int SPECTRAL_Y = 9; // Spectral class "D" (white dwarfs) are determined by their luminosity "VII" - the number is here for sorting public static final int SPECTRAL_D = 99; // "Q" - not a proper star (neutron stars QN, pulsars QP, black holes QB, ...) public static final int SPECTRAL_Q = 100; // TODO: Wolf-Rayet stars ("W"), carbon stars ("C"), S-type stars ("S"), public static final String LUM_0 = "0"; //$NON-NLS-1$ public static final String LUM_IA = "Ia"; //$NON-NLS-1$ public static final String LUM_IAB = "Iab"; //$NON-NLS-1$ public static final String LUM_IB = "Ib"; //$NON-NLS-1$ // Generic class, consisting of Ia, Iab and Ib public static final String LUM_I = "I"; //$NON-NLS-1$ public static final String LUM_II_EVOLVED = "I/II"; //$NON-NLS-1$ public static final String LUM_II = "II"; //$NON-NLS-1$ public static final String LUM_III_EVOLVED = "II/III"; //$NON-NLS-1$ public static final String LUM_III = "III"; //$NON-NLS-1$ public static final String LUM_IV_EVOLVED = "III/IV"; //$NON-NLS-1$ public static final String LUM_IV = "IV"; //$NON-NLS-1$ public static final String LUM_V_EVOLVED = "IV/V"; //$NON-NLS-1$ public static final String LUM_V = "V"; //$NON-NLS-1$ // typically used as a prefix "sd", not as a suffix public static final String LUM_VI = "VI"; //$NON-NLS-1$ // typically used as a prefix "esd", not as a suffix public static final String LUM_VI_PLUS = "VI+"; //$NON-NLS-1$ // always used as class designation "D", never as a suffix public static final String LUM_VII = "VII"; //$NON-NLS-1$ @XmlElement(name = "xcood") private Double x; @XmlElement(name = "ycood") private Double y; // Base data private String id; private String name; private String shortName; private Integer sysPos; //Star data (to be factored out) private String spectralType; @XmlJavaTypeAdapter(SpectralClassAdapter.class) private Integer spectralClass; private Double subtype; private String luminosity; // Orbital information /** Semimajor axis (average distance to parent star), in AU */ @XmlElement(name = "orbitRadius") private Double orbitSemimajorAxis; private Double orbitEccentricity; /** Degrees to the system's invariable plane */ private Double orbitInclination; // Stellar neighbourhood @XmlElement(name="satellites") private Integer numSatellites; @XmlElement(name="satellite") private List<String> satellites; // Global physical characteristics /** Mass in Earth masses */ private Double mass; /** Radius in Earth radii */ private Double radius; /** Density in kg/m^3 */ private Double density; private Double gravity; private Double dayLength; private Double tilt; @XmlElement(name = "class") private String className; // Surface description private Integer percentWater; @XmlElement(name = "volcanism") private Integer volcanicActivity; @XmlElement(name = "tectonics") private Integer tectonicActivity; @XmlElement(name="landMass") private List<String> landMasses; @XmlJavaTypeAdapter(BooleanValueAdapter.class) private Boolean nadirCharge; @XmlJavaTypeAdapter(BooleanValueAdapter.class) private Boolean zenithCharge; // Atmospheric description /** Pressure classification */ private Integer pressure; /** Pressure in standard pressure (101325 Pa) */ private Double pressureAtm; /** Atmospheric description */ private String atmosphere; /** Atmospheric mass compared to Earth's 28.9645 kg/mol */ private Double atmMass; private Double albedo; @XmlElement(name="greenhouse") private Double greenhouseEffect; /** Average surface temperature at equator in °C */ private Integer temperature; @XmlJavaTypeAdapter(ClimateAdapter.class) private Climate climate; // Ecosphere @XmlJavaTypeAdapter(LifeFormAdapter.class) private LifeForm lifeForm; private Integer habitability; // Human influence /** Order of magnitude of the population - 1 */ @XmlElement(name = "pop") private Integer populationRating; private String government; private Integer controlRating; @XmlJavaTypeAdapter(SocioIndustrialDataAdapter.class) private SocioIndustrialData socioIndustrial; @XmlJavaTypeAdapter(HPGRatingAdapter.class) private Integer hpg; @XmlElement(name = "faction") @XmlJavaTypeAdapter(StringListAdapter.class) private List<String> factions; //private List<String> garrisonUnits; // Fluff private String desc; private String icon; /** * a hash to keep track of dynamic planet changes * <p> * sorted map of [date of change: change information] * <p> * Package-private so that Planets can access it */ @XmlTransient TreeMap<DateTime, PlanetaryEvent> events; //a hash to keep track of dynamic garrison changes //TreeMap<DateTime, List<String>> garrisonHistory; /** @deprecated Use "event", which can have any number of changes to the planetary data */ @XmlElement(name = "factionChange") private List<FactionChange> factionChanges; // For export and import only (lists are easier than maps) */ @XmlElement(name = "event") private List<Planet.PlanetaryEvent> eventList; /** Marker for "please delete this planet" */ @XmlJavaTypeAdapter(BooleanValueAdapter.class) public Boolean delete; public Planet() { } public Planet(String id) { this.id = id; } // Constant base data public String getId() { return id; } public String getClassName() { return className; } public Double getGravity() { return gravity; } public Double getMass() { return mass; } public Double getDensity() { return density; } public Double getRadius() { return radius; } public String getGravityText() { return null != gravity ? gravity.toString() + "g" : "unknown"; //$NON-NLS-1$ } public Double getOrbitSemimajorAxis() { return orbitSemimajorAxis; } /** @return orbital semimajor axis in km; in the middle of the star's life zone if not set */ public double getOrbitSemimajorAxisKm() { return null != orbitSemimajorAxis ? orbitSemimajorAxis * StarUtil.AU : getStarAverageLifeZone(); } public List<String> getSatellites() { return null != satellites ? new ArrayList<String>(satellites) : null; } public String getSatelliteDescription() { if(null == satellites || satellites.isEmpty()) { return "0"; //$NON-NLS-1$ } return satellites.size() + " (" + Utilities.combineString(satellites, ", ") + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } public List<String> getLandMasses() { return null != landMasses ? new ArrayList<String>(landMasses) : null; } public String getLandMassDescription() { return null != landMasses ? Utilities.combineString(landMasses, ", ") : ""; //$NON-NLS-1$ //$NON-NLS-2$ } public Integer getVolcanicActivity() { return volcanicActivity; } public Integer getTectonicActivity() { return tectonicActivity; } public Double getDayLength() { return dayLength; } public Integer getSystemPosition() { return sysPos; } public String getSystemPositionText() { return null != sysPos ? sysPos.toString() : "?"; //$NON-NLS-1$ } public Double getOrbitEccentricity() { return orbitEccentricity; } public Double getOrbitInclination() { return orbitInclination; } public Double getTilt() { return tilt; } public String getDescription() { return desc; } public String getIcon() { return icon; } // Constant stellar data (to be moved out later) public Double getX() { return x; } public Double getY() { return y; } public String getSpectralType() { return spectralType; } /** @return normalized spectral type, for display */ public String getSpectralTypeNormalized() { return null != spectralType ? StarUtil.getSpectralType(spectralClass, subtype, luminosity) : "?"; //$NON-NLS-1$ } public String getSpectralTypeText() { if(null == spectralType || spectralType.isEmpty()) { return "unknown"; } if(spectralType.startsWith("Q")) { switch(spectralType) { case "QB": return "black hole"; //$NON-NLS-1$ case "QN": return "neutron star"; //$NON-NLS-1$ case "QP": return "pulsar"; //$NON-NLS-1$ default: return "unknown"; } } return spectralType; } public Integer getSpectralClass() { return spectralClass; } public void setSpectralClass(Integer spectralClass) { this.spectralClass = spectralClass; } public Double getSubtype() { return subtype; } public void setSubtype(double subtype) { this.subtype = subtype; } // Date-dependant data @SuppressWarnings("unchecked") public PlanetaryEvent getOrCreateEvent(DateTime when) { if(null == when) { return null; } if(null == events) { events = new TreeMap<DateTime, PlanetaryEvent>(DateTimeComparator.getDateOnlyInstance()); } PlanetaryEvent event = events.get(when); if(null == event) { event = new PlanetaryEvent(); event.date = when; events.put(when, event); } return event; } public PlanetaryEvent getEvent(DateTime when) { if((null == when) || (null == events)) { return null; } return events.get(when); } public List<PlanetaryEvent> getEvents() { if( null == events ) { return null; } return new ArrayList<PlanetaryEvent>(events.values()); } protected <T> T getEventData(DateTime when, T defaultValue, EventGetter<T> getter) { if( null == when || null == events || null == getter ) { return defaultValue; } T result = defaultValue; for( DateTime date : events.navigableKeySet() ) { if( date.isAfter(when) ) { break; } result = Utilities.nonNull(getter.get(events.get(date)), result); } return result; } /** @return events for this year. Never returns <i>null</i>. */ public List<PlanetaryEvent> getEvents(int year) { if( null == events ) { return Collections.<PlanetaryEvent>emptyList(); } List<PlanetaryEvent> result = new ArrayList<PlanetaryEvent>(); for( DateTime date : events.navigableKeySet() ) { if( date.getYear() > year ) { break; } if( date.getYear() == year ) { result.add(events.get(date)); } } return result; } public String getName(DateTime when) { return getEventData(when, name, new EventGetter<String>() { @Override public String get(PlanetaryEvent e) { return e.name; } }); } public String getShortName(DateTime when) { return getEventData(when, shortName, new EventGetter<String>() { @Override public String get(PlanetaryEvent e) { return e.shortName; } }); } /** @return short name if set, else full name, else "unnamed" */ public String getPrintableName(DateTime when) { String result = getShortName(when); if( null == result ) { result = getName(when); } return null != result ? result : "unnamed"; //$NON-NLS-1$ } public SocioIndustrialData getSocioIndustrial(DateTime when) { return getEventData(when, socioIndustrial, new EventGetter<SocioIndustrialData>() { @Override public SocioIndustrialData get(PlanetaryEvent e) { return e.socioIndustrial; } }); } public String getSocioIndustrialText(DateTime when) { SocioIndustrialData sid = getSocioIndustrial(when); return null != sid ? sid.toString() : ""; //$NON-NLS-1$ } public Integer getHPG(DateTime when) { return getEventData(when, hpg, new EventGetter<Integer>() { @Override public Integer get(PlanetaryEvent e) { return e.hpg; } }); } public String getHPGClass(DateTime when) { return StarUtil.getHPGClass(getHPG(when)); } public Integer getPopulationRating(DateTime when) { return getEventData(when, populationRating, new EventGetter<Integer>() { @Override public Integer get(PlanetaryEvent e) { return e.populationRating; } }); } public String getPopulationRatingString(DateTime when) { Integer pops = getPopulationRating(when); return (null != pops) ? StarUtil.getPopulationRatingString(pops.intValue()) : "unknown"; } public String getGovernment(DateTime when) { return getEventData(when, government, new EventGetter<String>() { @Override public String get(PlanetaryEvent e) { return e.government; } }); } public Integer getControlRating(DateTime when) { return getEventData(when, controlRating, new EventGetter<Integer>() { @Override public Integer get(PlanetaryEvent e) { return e.controlRating; } }); } public String getControlRatingString(DateTime when) { Integer cr = getControlRating(when); return (null != cr) ? StarUtil.getControlRatingString(cr.intValue()) : "actual situation unclear"; } public LifeForm getLifeForm(DateTime when) { return getEventData(when, null != lifeForm ? lifeForm : LifeForm.NONE, new EventGetter<LifeForm>() { @Override public LifeForm get(PlanetaryEvent e) { return e.lifeForm; } }); } public String getLifeFormName(DateTime when) { return getLifeForm(when).name; } public Climate getClimate(DateTime when) { return getEventData(when, climate, new EventGetter<Climate>() { @Override public Climate get(PlanetaryEvent e) { return e.climate; } }); } public String getClimateName(DateTime when) { Climate c = getClimate(when); return null != c ? c.climateName : null; } public Integer getPercentWater(DateTime when) { return getEventData(when, percentWater, new EventGetter<Integer>() { @Override public Integer get(PlanetaryEvent e) { return e.percentWater; } }); } public Integer getTemperature(DateTime when) { return getEventData(when, temperature, new EventGetter<Integer>() { @Override public Integer get(PlanetaryEvent e) { return e.temperature; } }); } public Integer getPressure(DateTime when) { return getEventData(when, pressure, new EventGetter<Integer>() { @Override public Integer get(PlanetaryEvent e) { return e.pressure; } }); } public String getPressureName(DateTime when) { Integer currentPressure = getPressure(when); return null != currentPressure ? PlanetaryConditions.getAtmosphereDisplayableName(currentPressure) : "unknown"; } public Double getPressureAtm(DateTime when) { return getEventData(when, pressureAtm, new EventGetter<Double>() { @Override public Double get(PlanetaryEvent e) { return e.pressureAtm; } }); } public Double getAtmMass(DateTime when) { return getEventData(when, atmMass, new EventGetter<Double>() { @Override public Double get(PlanetaryEvent e) { return e.atmMass; } }); } public String getAtmosphere(DateTime when) { return getEventData(when, atmosphere, new EventGetter<String>() { @Override public String get(PlanetaryEvent e) { return e.atmosphere; } }); } public Double getAlbedo(DateTime when) { return getEventData(when, albedo, new EventGetter<Double>() { @Override public Double get(PlanetaryEvent e) { return e.albedo; } }); } public Double getGreenhouseEffect(DateTime when) { return getEventData(when, greenhouseEffect, new EventGetter<Double>() { @Override public Double get(PlanetaryEvent e) { return e.greenhouseEffect; } }); } public Integer getHabitability(DateTime when) { return getEventData(when, habitability, new EventGetter<Integer>() { @Override public Integer get(PlanetaryEvent e) { return e.habitability; } }); } public List<String> getFactions(DateTime when) { return getEventData(when, factions, new EventGetter<List<String>>() { @Override public List<String> get(PlanetaryEvent e) { return e.faction; } }); } private static Set<Faction> getFactionsFrom(Collection<String> codes) { Set<Faction> factions = new HashSet<Faction>(codes.size()); for(String code : codes) { factions.add(Faction.getFaction(code)); } return factions; } /** @return set of factions at a given date */ public Set<Faction> getFactionSet(DateTime when) { List<String> currentFactions = getFactions(when); return null != currentFactions ? getFactionsFrom(currentFactions) : null; } public String getShortDesc(DateTime when) { return getShortName(when) + " (" + getFactionDesc(when) + ")"; //$NON-NLS-1$ //$NON-NLS-2$ } public String getFactionDesc(DateTime when) { int era = Era.getEra(when.getYear()); return Faction.getFactionNames(getFactionSet(when), era); } // Stellar event data, to be moved public Boolean isNadirCharge(DateTime when) { return getEventData(when, nadirCharge, new EventGetter<Boolean>() { @Override public Boolean get(PlanetaryEvent e) { return e.nadirCharge; } }); } public boolean isZenithCharge(DateTime when) { return getEventData(when, zenithCharge, new EventGetter<Boolean>() { @Override public Boolean get(PlanetaryEvent e) { return e.zenithCharge; } }); } public String getRechargeStationsText(DateTime when) { Boolean nadir = isNadirCharge(when); Boolean zenith = isZenithCharge(when); if(null != nadir && null != zenith && nadir.booleanValue() && zenith.booleanValue()) { return "Zenith, Nadir"; } else if(null != zenith && zenith.booleanValue()) { return "Zenith"; } else if(null != nadir && nadir.booleanValue()) { return "Nadir"; } else { return "None"; } } /** Recharge time in hours (assuming the usage of the fastest charing method available) */ public double getRechargeTime(DateTime when) { if(isZenithCharge(when) || isNadirCharge(when)) { return Math.min(176.0, 141 + 10*spectralClass + subtype); } else { return getSolarRechargeTime(); } } /** Recharge time in hours using solar radiation alone (at jump point and 100% efficiency) */ public double getSolarRechargeTime() { if( null == spectralClass || null == subtype ) { return 183; } return StarUtil.getSolarRechargeTime(spectralClass, subtype); } public String getRechargeTimeText(DateTime when) { double time = getRechargeTime(when); if(Double.isInfinite(time)) { return "recharging impossible"; //$NON-NLS-1$ } else { return String.format("%.0f hours", time); //$NON-NLS-1$ } } // Astronavigation /** @return the average travel time from low orbit to the jump point at 1g, in Terran days */ public double getTimeToJumpPoint(double acceleration) { //based on the formula in StratOps return Math.sqrt((getDistanceToJumpPoint() * 1000) / (StarUtil.G * acceleration)) / 43200; } /** @return the average distance to the system's jump point in km */ public double getDistanceToJumpPoint() { return Math.sqrt(Math.pow(getOrbitSemimajorAxisKm(), 2) + Math.pow(getStarDistanceToJumpPoint(), 2)); } private double getStarDistanceToJumpPoint() { if( null == spectralClass || null == subtype ) { return StarUtil.getDistanceToJumpPoint(42); } return StarUtil.getDistanceToJumpPoint(spectralClass, subtype); } /** @return the rough middle of the habitable zone around this star, in km */ private double getStarAverageLifeZone() { // TODO Calculate from luminosity and the like. For now, using the table in IO Beta. if( null == spectralClass || null == subtype ) { return (StarUtil.getMinLifeZone(42) + StarUtil.getMaxLifeZone(42)) / 2; } return (StarUtil.getMinLifeZone(spectralClass, subtype) + StarUtil.getMaxLifeZone(spectralClass, subtype)) / 2; } /** @return the distance to another planet in light years (0 if both are in the same system) */ public double getDistanceTo(Planet anotherPlanet) { return Math.sqrt(Math.pow(x - anotherPlanet.x, 2) + Math.pow(y - anotherPlanet.y, 2)); } /** @return the distance to a point in space in light years */ public double getDistanceTo(double x, double y) { return Math.sqrt(Math.pow(x - this.x, 2) + Math.pow(y - this.y, 2)); } // JAXB marshalling support @SuppressWarnings({ "unused", "unchecked" }) private void afterUnmarshal(Unmarshaller unmarshaller, Object parent) { if( null == id ) { id = name; } // Spectral classification: use spectralType if available, else the separate values if( null != spectralType ) { setSpectralType(spectralType); } else { spectralType = StarUtil.getSpectralType(spectralClass, subtype, luminosity); } nadirCharge = Utilities.nonNull(nadirCharge, Boolean.FALSE); zenithCharge = Utilities.nonNull(zenithCharge, Boolean.FALSE); // Generate a bunch of data if we still don't have it if( null == spectralType ) { setSpectralType(StarUtil.generateSpectralType( new Random(id.hashCode() + 133773), true, (null != spectralClass) ? spectralClass.intValue() : -1)); } // Fill up events events = new TreeMap<DateTime, PlanetaryEvent>(DateTimeComparator.getDateOnlyInstance()); if( null != eventList ) { for( PlanetaryEvent event : eventList ) { if( null != event && null != event.date ) { events.put(event.date, event); } } eventList.clear(); } eventList = null; // Merge faction change events into the event data if( null != factionChanges ) { for( FactionChange change : factionChanges ) { if( null != change && null != change.date ) { PlanetaryEvent event = getOrCreateEvent(change.date); event.faction = change.faction; } } factionChanges.clear(); } factionChanges = null; } @SuppressWarnings("unused") private boolean beforeMarshal(Marshaller marshaller) { // Fill up our event list from the internal data type eventList = new ArrayList<PlanetaryEvent>(events.values()); return true; } /** Includes a parser for spectral type strings */ protected void setSpectralType(String type) { SpectralDefinition scDef = StarUtil.parseSpectralType(type); if( null == scDef ) { return; } spectralType = scDef.spectralType; spectralClass = scDef.spectralClass; subtype = scDef.subtype; luminosity = scDef.luminosity; } /** * Copy all but id from the other planet. Update event list. Events with the * same date as others already in the list get overwritten, others added. * To effectively delete an event, simply create a new one with <i>just</i> the date. */ public void copyDataFrom(Planet other) { if( null != other ) { // We don't change the ID name = Utilities.nonNull(other.name, name); shortName = Utilities.nonNull(other.shortName, shortName); x = Utilities.nonNull(other.x, x); y = Utilities.nonNull(other.y, y); spectralType = Utilities.nonNull(other.spectralType, spectralType); spectralClass =Utilities.nonNull(other.spectralClass, spectralClass); subtype = Utilities.nonNull(other.subtype, subtype); luminosity = Utilities.nonNull(other.luminosity, luminosity); climate = Utilities.nonNull(other.climate, climate); desc = Utilities.nonNull(other.desc, desc); factions = Utilities.nonNull(other.factions, factions); gravity = Utilities.nonNull(other.gravity, gravity); hpg = Utilities.nonNull(other.hpg, hpg); landMasses = Utilities.nonNull(other.landMasses, landMasses); lifeForm = Utilities.nonNull(other.lifeForm, lifeForm); orbitSemimajorAxis = Utilities.nonNull(other.orbitSemimajorAxis, orbitSemimajorAxis); orbitEccentricity = Utilities.nonNull(other.orbitEccentricity, orbitEccentricity); orbitInclination = Utilities.nonNull(other.orbitInclination, orbitInclination); percentWater = Utilities.nonNull(other.percentWater, percentWater); pressure = Utilities.nonNull(other.pressure, pressure); pressureAtm = Utilities.nonNull(other.pressureAtm, pressureAtm); pressureAtm = Utilities.nonNull(other.pressureAtm, pressureAtm); atmMass = Utilities.nonNull(other.atmMass, atmMass); atmosphere = Utilities.nonNull(other.atmosphere, atmosphere); albedo = Utilities.nonNull(other.albedo, albedo); greenhouseEffect = Utilities.nonNull(other.greenhouseEffect, greenhouseEffect); volcanicActivity = Utilities.nonNull(other.volcanicActivity, volcanicActivity); tectonicActivity = Utilities.nonNull(other.tectonicActivity, tectonicActivity); populationRating = Utilities.nonNull(other.populationRating, populationRating); government = Utilities.nonNull(other.government, government); controlRating = Utilities.nonNull(other.controlRating, controlRating); habitability = Utilities.nonNull(other.habitability, habitability); dayLength = Utilities.nonNull(other.dayLength, dayLength); satellites = Utilities.nonNull(other.satellites, satellites); sysPos = Utilities.nonNull(other.sysPos, sysPos); temperature = Utilities.nonNull(other.temperature, temperature); socioIndustrial = Utilities.nonNull(other.socioIndustrial, socioIndustrial); icon = Utilities.nonNull(other.icon, icon); // Merge (not replace!) events if( null != other.events ) { for( PlanetaryEvent event : other.getEvents() ) { if( null != event && null != event.date ) { PlanetaryEvent myEvent = getOrCreateEvent(event.date); myEvent.copyDataFrom(event); } } } } } @Override public int hashCode() { return Objects.hashCode(id); } @Override public boolean equals(Object object) { if(this == object) { return true; } if((null == object) || (getClass() != object.getClass())) { return false; } final Planet other = (Planet) object; return Objects.equals(id, other.id); } public static int convertRatingToCode(String rating) { if(rating.equalsIgnoreCase("A")) { //$NON-NLS-1$ return EquipmentType.RATING_A; } else if(rating.equalsIgnoreCase("B")) { //$NON-NLS-1$ return EquipmentType.RATING_B; } else if(rating.equalsIgnoreCase("C")) { //$NON-NLS-1$ return EquipmentType.RATING_C; } else if(rating.equalsIgnoreCase("D")) { //$NON-NLS-1$ return EquipmentType.RATING_D; } else if(rating.equalsIgnoreCase("E")) { //$NON-NLS-1$ return EquipmentType.RATING_E; } else if(rating.equalsIgnoreCase("F")) { //$NON-NLS-1$ return EquipmentType.RATING_F; } return EquipmentType.RATING_C; } public static final class SocioIndustrialData { public static final SocioIndustrialData NONE = new SocioIndustrialData(); static { NONE.tech = EquipmentType.RATING_X; NONE.industry = EquipmentType.RATING_X; NONE.rawMaterials = EquipmentType.RATING_X; NONE.output = EquipmentType.RATING_X; NONE.agriculture = EquipmentType.RATING_X; } public int tech; public int industry; public int rawMaterials; public int output; public int agriculture; @Override public String toString() { return EquipmentType.getRatingName(tech) + "-" + EquipmentType.getRatingName(industry) //$NON-NLS-1$ + "-" + EquipmentType.getRatingName(rawMaterials) //$NON-NLS-1$ + "-" + EquipmentType.getRatingName(output) //$NON-NLS-1$ + "-" + EquipmentType.getRatingName(agriculture); //$NON-NLS-1$ } /** @return the USILR rating as a HTML description */ public String getHTMLDescription() { // TODO: Internationalization // TODO: Some way to encode "advanced" ultra-tech worlds (rating "AA" for technological sophistication) // TODO: Some way to encode "regressed" worlds // Note that rating "E" isn't used in official USILR codes, but we add them for completeness StringBuilder sb = new StringBuilder("<html><body style='width: 50px; font: 10px sans-serif'>"); switch(tech) { case -1: sb.append("Advanced: Ultra high-tech world<br>"); break; case EquipmentType.RATING_A: sb.append("A: High-tech world<br>"); break; case EquipmentType.RATING_B: sb.append("B: Advanced world<br>"); break; case EquipmentType.RATING_C: sb.append("C: Moderately advanced world<br>"); break; case EquipmentType.RATING_D: sb.append("D: Lower-tech world; about 21st- to 22nd-century level<br>"); break; case EquipmentType.RATING_E: sb.append("E: Lower-tech world; about 20th century level<br>"); break; case EquipmentType.RATING_F: sb.append("F: Primitive world<br>"); break; case EquipmentType.RATING_X: sb.append("Regressed: Pre-industrial world<br>"); break; default: sb.append("X: Technological sophistication unknown<br>"); break; } switch(industry) { case EquipmentType.RATING_A: sb.append("A: Heavily industrialized<br>"); break; case EquipmentType.RATING_B: sb.append("B: Moderately industrialized<br>"); break; case EquipmentType.RATING_C: sb.append("C: Basic heavy industry; about 22nd century level<br>"); break; case EquipmentType.RATING_D: sb.append("D: Low industrialization; about 20th century level<br>"); break; case EquipmentType.RATING_E: sb.append("E: Very low industrialization; about 19th century level<br>"); break; case EquipmentType.RATING_F: sb.append("F: No industrialization<br>"); break; default: sb.append("X: Industrialization level unknown<br>"); break; } switch(rawMaterials) { case EquipmentType.RATING_A: sb.append("A: Fully self-sufficient raw material production<br>"); break; case EquipmentType.RATING_B: sb.append("B: Mostly self-sufficient raw material production<br>"); break; case EquipmentType.RATING_C: sb.append("C: Limited raw material production<br>"); break; case EquipmentType.RATING_D: sb.append("D: Production dependent on imports of raw materials<br>"); break; case EquipmentType.RATING_E: sb.append("E: Production highly dependent on imports of raw materials<br>"); break; case EquipmentType.RATING_F: sb.append("F: No economically viable local raw material production<br>"); break; default: sb.append("X: Raw material dependence unknown<br>"); break; } switch(output) { case EquipmentType.RATING_A: sb.append("A: High industrial output<br>"); break; case EquipmentType.RATING_B: sb.append("B: Good industrial output<br>"); break; case EquipmentType.RATING_C: sb.append("C: Limited industrial output<br>"); // Bad for Ferengi break; case EquipmentType.RATING_D: sb.append("D: Negligable industrial output<br>"); break; case EquipmentType.RATING_E: sb.append("E: Negligable industrial output<br>"); break; case EquipmentType.RATING_F: sb.append("F: No industrial output<br>"); // Good for Ferengi break; default: sb.append("X: Industrial output unknown<br>"); break; } switch(agriculture) { case EquipmentType.RATING_A: sb.append("A: Breadbasket<br>"); break; case EquipmentType.RATING_B: sb.append("B: Agriculturally abundant world<br>"); break; case EquipmentType.RATING_C: sb.append("C: Modest agriculture<br>"); break; case EquipmentType.RATING_D: sb.append("D: Poor agriculture<br>"); break; case EquipmentType.RATING_E: sb.append("E: Very poor agriculture<br>"); break; case EquipmentType.RATING_F: sb.append("F: Barren world<br>"); break; default: sb.append("X: Agricultural level unknown<br>"); break; } return sb.append("</body></html>").toString(); } } /** A class representing some event, possibly changing planetary information */ @XmlRootElement(name="event") public static final class PlanetaryEvent { @XmlJavaTypeAdapter(DateAdapter.class) public DateTime date; public String message; public String name; public String shortName; @XmlJavaTypeAdapter(StringListAdapter.class) public List<String> faction; @XmlJavaTypeAdapter(LifeFormAdapter.class) public LifeForm lifeForm; @XmlJavaTypeAdapter(ClimateAdapter.class) public Climate climate; public Integer percentWater; public Integer temperature; @XmlJavaTypeAdapter(SocioIndustrialDataAdapter.class) public SocioIndustrialData socioIndustrial; @XmlJavaTypeAdapter(HPGRatingAdapter.class) public Integer hpg; public Integer pressure; public Double pressureAtm; public Double atmMass; public String atmosphere; public Double albedo; public Double greenhouseEffect; public Integer habitability; @XmlElement(name = "pop") public Integer populationRating; public String government; public Integer controlRating; // Stellar support, to be moved later public Boolean nadirCharge; public Boolean zenithCharge; // Events marked as "custom" are saved to scenario files and loaded from there public transient boolean custom = false; public void copyDataFrom(PlanetaryEvent other) { climate = Utilities.nonNull(other.climate, climate); faction = Utilities.nonNull(other.faction, faction); hpg = Utilities.nonNull(other.hpg, hpg); lifeForm = Utilities.nonNull(other.lifeForm, lifeForm); message = Utilities.nonNull(other.message, message); name = Utilities.nonNull(other.name, name); percentWater = Utilities.nonNull(other.percentWater, percentWater); shortName = Utilities.nonNull(other.shortName, shortName); socioIndustrial = Utilities.nonNull(other.socioIndustrial, socioIndustrial); temperature = Utilities.nonNull(other.temperature, temperature); pressure = Utilities.nonNull(other.pressure, pressure); pressureAtm = Utilities.nonNull(other.pressureAtm, pressureAtm); atmMass = Utilities.nonNull(other.atmMass, atmMass); atmosphere = Utilities.nonNull(other.atmosphere, atmosphere); albedo = Utilities.nonNull(other.albedo, albedo); greenhouseEffect = Utilities.nonNull(other.greenhouseEffect, greenhouseEffect); habitability = Utilities.nonNull(other.habitability, habitability); populationRating = Utilities.nonNull(other.populationRating, populationRating); government = Utilities.nonNull(other.government, government); controlRating = Utilities.nonNull(other.controlRating, controlRating); nadirCharge = Utilities.nonNull(other.nadirCharge, nadirCharge); zenithCharge = Utilities.nonNull(other.zenithCharge, zenithCharge); custom = (other.custom || custom); } public void replaceDataFrom(PlanetaryEvent other) { climate = other.climate; faction = other.faction; hpg = other.hpg; lifeForm = other.lifeForm; message = other.message; name = other.name; percentWater = other.percentWater; shortName = other.shortName; socioIndustrial = other.socioIndustrial; temperature = other.temperature; pressure = other.pressure; pressureAtm = other.pressureAtm; atmMass = other.atmMass; atmosphere = other.atmosphere; albedo = other.albedo; greenhouseEffect = other.greenhouseEffect; habitability = other.habitability; populationRating = other.populationRating; government = other.government; controlRating = other.controlRating; nadirCharge = other.nadirCharge; zenithCharge = other.zenithCharge; custom = (other.custom || custom); } /** @return <code>true</code> if the event doesn't contain any change */ public boolean isEmpty() { return (null == climate) && (null == faction) && (null == hpg) && (null == lifeForm) && (null == message) && (null == name) && (null == shortName) && (null == socioIndustrial) && (null == temperature) && (null == pressure) && (null == pressureAtm) && (null == atmMass) && (null == atmosphere) && (null == albedo) && (null == greenhouseEffect) && (null == habitability) && (null == populationRating) && (null == government) && (null == controlRating) && (null == nadirCharge) && (null == zenithCharge); } } public static final class FactionChange { @XmlJavaTypeAdapter(DateAdapter.class) public DateTime date; @XmlJavaTypeAdapter(StringListAdapter.class) public List<String> faction; @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("{"); //$NON-NLS-1$ sb.append("date=").append(date).append(","); //$NON-NLS-1$ //$NON-NLS-2$ sb.append("faction=").append(faction).append("}"); //$NON-NLS-1$ //$NON-NLS-2$ return sb.toString(); } } // @FunctionalInterface in Java 8, or just use Function<PlanetaryEvent, T> private static interface EventGetter<T> { T get(PlanetaryEvent e); } /** BT planet types */ public static enum PlanetaryType { SMALL_ASTEROID, MEDIUM_ASTEROID, DWARF_TERRESTRIAL, TERRESTRIAL, GIANT_TERRESTRIAL, GAS_GIANT, ICE_GIANT; } /** Data class to hold parsed spectral definitions */ public static final class SpectralDefinition { public String spectralType; public int spectralClass; public double subtype; public String luminosity; public SpectralDefinition(String spectralType, int spectralClass, double subtype, String luminosity) { this.spectralType = Objects.requireNonNull(spectralType); this.spectralClass = spectralClass; this.subtype = subtype; this.luminosity = Objects.requireNonNull(luminosity); } } }