/*
* Copyright (C) 2011-2016 MegaMek team
*
* 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.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.transform.stream.StreamSource;
import org.joda.time.DateTime;
import org.w3c.dom.DOMException;
import org.w3c.dom.Node;
import megamek.common.EquipmentType;
import mekhq.FileParser;
import mekhq.MekHQ;
import mekhq.Utilities;
public class Planets {
private final static Object LOADING_LOCK = new Object[0];
private static Planets planets;
// Marshaller / unmarshaller instances
private static Marshaller marshaller;
private static Unmarshaller unmarshaller;
static {
try {
JAXBContext context = JAXBContext.newInstance(LocalPlanetList.class, Planet.class);
marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
unmarshaller = context.createUnmarshaller();
// For debugging only!
// unmarshaller.setEventHandler(new javax.xml.bind.helpers.DefaultValidationEventHandler());
} catch(JAXBException e) {
MekHQ.logError(e);
}
}
public static Planets getInstance() {
if(planets == null) {
planets = new Planets();
}
if(!planets.initialized && !planets.initializing) {
planets.initializing = true;
planets.loader = new Thread(new Runnable() {
@Override
public void run() {
planets.initialize();
}
}, "Planet Loader");
planets.loader.setPriority(Thread.NORM_PRIORITY - 1);
planets.loader.start();
}
return planets;
}
public static void reload(boolean waitForFinish) {
planets = null;
getInstance();
if(waitForFinish) {
try {
while(!planets.isInitialized()) {
Thread.sleep(10);
}
} catch(InterruptedException iex) {
MekHQ.logError(iex);
}
}
}
private ConcurrentMap<String, Planet> planetList = new ConcurrentHashMap<>();
/* organizes systems into a grid of 30lyx30ly squares so we can find
* nearby systems without iterating through the entire planet list. */
private HashMap<Integer, Map<Integer, Set<Planet>>> planetGrid = new HashMap<>();
// HPG Network cache (to not recalculate all the damn time)
private Collection<Planets.HPGLink> hpgNetworkCache = null;
private DateTime hpgNetworkCacheDate = null;
private Thread loader;
private boolean initialized = false;
private boolean initializing = false;
private Planets() {}
private Set<Planet> getPlanetGrid(int x, int y) {
if( !planetGrid.containsKey(x) ) {
return null;
}
return planetGrid.get(x).get(y);
}
public List<Planet> getNearbyPlanets(final double centerX, final double centerY, int distance) {
List<Planet> neighbors = new ArrayList<>();
int gridRadius = (int)Math.ceil(distance / 30.0);
int gridX = (int)(centerX / 30.0);
int gridY = (int)(centerY / 30.0);
for (int x = gridX - gridRadius; x <= gridX + gridRadius; x++) {
for (int y = gridY - gridRadius; y <= gridY + gridRadius; y++) {
Set<Planet> grid = getPlanetGrid(x, y);
if(null != grid) {
for(Planet p : grid) {
if(p.getDistanceTo(centerX, centerY) <= distance) {
neighbors.add(p);
}
}
}
}
}
Collections.sort(neighbors, new Comparator<Planet>() {
@Override
public int compare(Planet o1, Planet o2) {
return Double.compare(o1.getDistanceTo(centerX, centerY), o2.getDistanceTo(centerX, centerY));
}
});
return neighbors;
}
public List<Planet> getNearbyPlanets(final Planet planet, int distance) {
return getNearbyPlanets(planet.getX(), planet.getY(), distance);
}
public ConcurrentMap<String, Planet> getPlanets() {
return planetList;
}
public Planet getPlanetById(String id) {
return( null != id ? planetList.get(id) : null);
}
/** Return the planet by given name at a given time point */
public Planet getPlanetByName(String name, DateTime when) {
if(null == name) {
return null;
}
name = name.toLowerCase(Locale.ROOT);
for(Planet planet : planetList.values()) {
if(null != planet) {
String planetName = planet.getName(when);
if((null != planetName) && planetName.toLowerCase(Locale.ROOT).equals(name)) {
return planet;
}
planetName = planet.getShortName(when);
if((null != planetName) && planetName.toLowerCase(Locale.ROOT).equals(name)) {
return planet;
}
}
}
return null;
}
public List<NewsItem> getPlanetaryNews(DateTime when) {
List<NewsItem> news = new ArrayList<>();
for(Planet planet : planetList.values()) {
if(null != planet) {
Planet.PlanetaryEvent event = planet.getEvent(when);
if((null != event) && (null != event.message)) {
NewsItem item = new NewsItem();
item.setHeadline(event.message);
item.setDate(event.date);
item.setLocation(planet.getPrintableName(when));
news.add(item);
}
}
}
return news;
}
/** Clean up the local HPG network cache */
public void recalcHPGNetwork() {
hpgNetworkCacheDate = null;
}
public Collection<Planets.HPGLink> getHPGNetwork(DateTime when) {
if((null != when) && when.equals(hpgNetworkCacheDate)) {
return hpgNetworkCache;
}
Set<HPGLink> result = new HashSet<>();
for(Planet planet : planetList.values()) {
Integer hpg = planet.getHPG(when);
if((null != hpg) && (hpg.intValue() == EquipmentType.RATING_A)) {
Collection<Planet> neighbors = getNearbyPlanets(planet, 50);
for(Planet neighbor : neighbors) {
hpg = neighbor.getHPG(when);
if(null != hpg) {
HPGLink link = new HPGLink(planet, neighbor, hpg.intValue());
if(!result.contains(link)) {
result.add(link);
}
}
}
}
}
hpgNetworkCache = result;
hpgNetworkCacheDate = when;
return result;
}
// Customisation and export helper methods
/** @return <code>true</code> if the planet was known and got updated, <code>false</code> otherwise */
public boolean updatePlanetaryEvents(String id, Collection<Planet.PlanetaryEvent> events) {
return updatePlanetaryEvents(id, events, false);
}
/** @return <code>true</code> if the planet was known and got updated, <code>false</code> otherwise */
public boolean updatePlanetaryEvents(String id, Collection<Planet.PlanetaryEvent> events, boolean replace) {
Planet planet = getPlanetById(id);
if(null == planet) {
return false;
}
if(null != events) {
for(Planet.PlanetaryEvent event : events) {
if(null != event.date) {
Planet.PlanetaryEvent planetaryEvent = planet.getOrCreateEvent(event.date);
if(replace) {
planetaryEvent.replaceDataFrom(event);
} else {
planetaryEvent.copyDataFrom(event);
}
}
}
}
return true;
}
public void writePlanet(OutputStream out, Planet planet) {
try {
marshaller.marshal(planet, out);
} catch (Exception e) {
MekHQ.logError(e);
}
}
public void writePlanet(Writer out, Planet planet) {
try {
marshaller.marshal(planet, out);
} catch (Exception e) {
MekHQ.logError(e);
}
}
public void writePlanetaryEvent(OutputStream out, Planet.PlanetaryEvent event) {
try {
marshaller.marshal(event, out);
} catch (Exception e) {
MekHQ.logError(e);
}
}
public void writePlanetaryEvent(Writer out, Planet.PlanetaryEvent event) {
try {
marshaller.marshal(event, out);
} catch (Exception e) {
MekHQ.logError(e);
}
}
public Planet.PlanetaryEvent readPlanetaryEvent(Node node) {
try {
return (Planet.PlanetaryEvent) unmarshaller.unmarshal(node);
} catch (JAXBException e) {
MekHQ.logError(e);
}
return null;
}
public void writePlanets(OutputStream out, List<Planet> planets) {
LocalPlanetList temp = new LocalPlanetList();
temp.list = planets;
try {
marshaller.marshal(temp, out);
} catch (Exception e) {
MekHQ.logError(e);
}
}
// Data loading methods
private void initialize() {
try {
generatePlanets();
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void done() {
initialized = true;
initializing = false;
}
public boolean isInitialized() {
return initialized;
}
private void updatePlanets(FileInputStream source) {
// JAXB unmarshaller closes the stream it doesn't own. Bad JAXB. BAD.
try(InputStream is = new FilterInputStream(source) {
@Override
public void close() { /* ignore */ }
}) {
// Reset the file stream
source.getChannel().position(0);
LocalPlanetList planets = unmarshaller.unmarshal(
new StreamSource(is), LocalPlanetList.class).getValue();
// Run through the list again, this time creating and updating planets as we go
for( Planet planet : planets.list ) {
Planet oldPlanet = planetList.get(planet.getId());
if( null == oldPlanet ) {
planetList.put(planet.getId(), planet);
} else {
// Update with new data
oldPlanet.copyDataFrom(planet);
planet = oldPlanet;
}
}
// Process planet deletions
for( String planetId : planets.toDelete ) {
if( null != planetId ) {
planetList.remove(planetId);
}
}
} catch (JAXBException e) {
MekHQ.logError(e);
} catch(IOException e) {
MekHQ.logError(e);
}
}
private void generatePlanets() throws DOMException, ParseException {
MekHQ.logMessage("Starting load of planetary data from XML..."); //$NON-NLS-1$
long currentTime = System.currentTimeMillis();
synchronized (LOADING_LOCK) {
// Step 1: Initialize variables.
if( null == planetList ) {
planetList = new ConcurrentHashMap<>();
}
planetList.clear();
if( null == planetGrid ) {
planetGrid = new HashMap<>();
}
// Be nice to the garbage collector
for( Map.Entry<Integer, Map<Integer, Set<Planet>>> planetGridColumn : planetGrid.entrySet() ) {
for( Map.Entry<Integer, Set<Planet>> planetGridElement : planetGridColumn.getValue().entrySet() ) {
if( null != planetGridElement.getValue() ) {
planetGridElement.getValue().clear();
}
}
if( null != planetGridColumn.getValue() ) {
planetGridColumn.getValue().clear();
}
}
planetGrid.clear();
// Step 2: Read the default file
try(FileInputStream fis = new FileInputStream("data/universe/planets.xml")) { //$NON-NLS-1$
updatePlanets(fis);
} catch (Exception ex) {
MekHQ.logError(ex);
}
// Step 3: Load all the xml files within the planets subdirectory, if it exists
Utilities.parseXMLFiles("data/universe/planets", //$NON-NLS-1$
new FileParser() {
@Override
public void parse(FileInputStream is) {
updatePlanets(is);
}
});
List<Planet> toRemove = new ArrayList<>();
for (Planet planet : planetList.values()) {
if((null == planet.getX()) || (null == planet.getY())) {
MekHQ.logError(String.format("Planet \"%s\" is missing coordinates", planet.getId())); //$NON-NLS-1$
toRemove.add(planet);
continue;
}
int x = (int)(planet.getX()/30.0);
int y = (int)(planet.getY()/30.0);
if (planetGrid.get(x) == null) {
planetGrid.put(x, new HashMap<Integer, Set<Planet>>());
}
if (planetGrid.get(x).get(y) == null) {
planetGrid.get(x).put(y, new HashSet<Planet>());
}
if( !planetGrid.get(x).get(y).contains(planet) ) {
planetGrid.get(x).get(y).add(planet);
}
}
for(Planet planet : toRemove) {
planetList.remove(planet.getId());
}
done();
}
MekHQ.logMessage(String.format(Locale.ROOT,
"Loaded a total of %d planets in %.3fs.", //$NON-NLS-1$
planetList.size(), (System.currentTimeMillis() - currentTime) / 1000.0));
// Planetary sanity check time!
for(Planet planet : planetList.values()) {
List<Planet> veryClosePlanets = getNearbyPlanets(planet, 1);
if(veryClosePlanets.size() > 1) {
for(Planet closePlanet : veryClosePlanets) {
if(!planet.getId().equals(closePlanet.getId())) {
MekHQ.logMessage(String.format(Locale.ROOT,
"Extremly close planets detected. Data error? %s <-> %s: %.3f ly", //$NON-NLS-1$
planet.getId(), closePlanet.getId(), planet.getDistanceTo(closePlanet)));
}
}
}
}
}
@XmlRootElement(name="planets")
private static final class LocalPlanetList {
@XmlElement(name="planet")
public List<Planet> list;
@XmlTransient
public List<String> toDelete;
@SuppressWarnings("unused")
private void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
toDelete = new ArrayList<String>();
if( null == list ) {
list = new ArrayList<Planet>();
} else {
// Fill in the "toDelete" list
List<Planet> filteredList = new ArrayList<Planet>(list.size());
for( Planet planet : list ) {
if( null != planet.delete && planet.delete && null != planet.getId() ) {
toDelete.add(planet.getId());
} else {
filteredList.add(planet);
}
}
list = filteredList;
}
}
}
/** A data class representing a HPG link between two planets */
public static final class HPGLink {
/** In case of HPG-A to HPG-B networks, <code>primary</code> holds the HPG-A node. Else the order doesn't matter. */
public final Planet primary;
public final Planet secondary;
public final int rating;
public HPGLink(Planet primary, Planet secondary, int rating) {
this.primary = primary;
this.secondary = secondary;
this.rating = rating;
}
@Override
public int hashCode() {
return Objects.hash(primary, secondary, rating);
}
@Override
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if((null == obj) || (getClass() != obj.getClass())) {
return false;
}
final HPGLink other = (HPGLink) obj;
return Objects.equals(primary, other.primary) && Objects.equals(secondary, other.secondary)
&& (rating == other.rating);
}
}
}