{
        this.lastLocalizationConfig = lastConfig;
    }

package com.ctrip.framework.dal.cluster.client.cluster;

import com.ctrip.framework.dal.cluster.client.Cluster;
import com.ctrip.framework.dal.cluster.client.base.UnsupportedListenable;
import com.ctrip.framework.dal.cluster.client.config.ClusterConfigImpl;
import com.ctrip.framework.dal.cluster.client.config.DalConfigCustomizedOption;
import com.ctrip.framework.dal.cluster.client.config.LocalizationConfig;
import com.ctrip.framework.dal.cluster.client.database.Database;
import com.ctrip.framework.dal.cluster.client.database.DatabaseCategory;
import com.ctrip.framework.dal.cluster.client.exception.ClusterRuntimeException;
import com.ctrip.framework.dal.cluster.client.multihost.ClusterRouteStrategyConfig;
import com.ctrip.framework.dal.cluster.client.shard.DatabaseShard;
import com.ctrip.framework.dal.cluster.client.sharding.context.DbShardContext;
import com.ctrip.framework.dal.cluster.client.sharding.context.TableShardContext;
import com.ctrip.framework.dal.cluster.client.sharding.idgen.ClusterIdGeneratorConfig;
import java.sql.SQLException;
import java.util.*;

/**
 * @author c7ch23en
 */
public class DefaultCluster extends UnsupportedListenable<ClusterSwitchedEvent> implements Cluster {

    private ClusterConfigImpl clusterConfig;

    private Map<Integer, DatabaseShard> databaseShards = new HashMap<>();

    private ShardStrategyProxy shardStrategyProxy;

    private ClusterIdGeneratorConfig idGeneratorConfig;

    private ClusterRouteStrategyConfig routeStrategyConfig;

    private LocalizationConfig localizationConfig;

    private LocalizationConfig lastLocalizationConfig;

    private DalConfigCustomizedOption customizedOption;

    public DefaultCluster(ClusterConfigImpl clusterConfig) {
        this.clusterConfig = clusterConfig;
    }

    @Override
    public String getClusterName() {
        return clusterConfig.getClusterName();
    }

    @Override
    public ClusterType getClusterType() {
        return clusterConfig.getClusterType();
    }

    @Override
    public DatabaseCategory getDatabaseCategory() {
        return clusterConfig.getDatabaseCategory();
    }

    @Override
    public boolean dbShardingEnabled() {
        return databaseShards.size() > 1;
    }

    @Override
    public Integer getDbShard(String tableName, DbShardContext context) {
        return shardStrategyProxy.getDbShard(tableName, context);
    }

    @Override
    public Set<Integer> getAllDbShards() {
        return databaseShards.keySet();
    }

    @Override
    public boolean tableShardingEnabled(String tableName) {
        return shardStrategyProxy.tableShardingEnabled(tableName);
    }

    @Override
    public String getTableShard(String tableName, TableShardContext context) {
        return shardStrategyProxy.getTableShard(tableName, context);
    }

    @Override
    public Set<String> getAllTableShards(String tableName) {
        return shardStrategyProxy.getAllTableShards(tableName);
    }

    @Override
    public String getTableShardSeparator(String tableName) {
        return shardStrategyProxy.getTableShardSeparator(tableName);
    }

    @Override
    public List<Database> getDatabases() {
        List<Database> databases = new LinkedList<>();
        for (DatabaseShard databaseShard : databaseShards.values()) {
            databases.addAll(databaseShard.getMasters());
            databases.addAll(databaseShard.getSlaves());
        }
        return databases;
    }

    @Override
    public Database getMasterOnShard(int shardIndex) {
        List<Database> masters = databaseShards.get(shardIndex).getMasters();
        if (masters == null || masters.isEmpty())
            throw new ClusterRuntimeException("no master available");
        // throw new ClusterRuntimeException("multi masters available");
        return masters.iterator().next();
    }

    @Override
    public List<Database> getSlavesOnShard(int shardIndex) {
        return databaseShards.get(shardIndex).getSlaves();
    }

    @Override
    public ClusterIdGeneratorConfig getIdGeneratorConfig() {
        return idGeneratorConfig;
    }

    @Override
    public ClusterRouteStrategyConfig getRouteStrategyConfig() {
        return routeStrategyConfig;
    }

    @Override
    public LocalizationConfig getLocalizationConfig() {
        return localizationConfig;
    }

    @Override
    public LocalizationConfig getLastLocalizationConfig() {
        return this.lastLocalizationConfig;
    }

    @Override
    public DalConfigCustomizedOption getCustomizedOption() {
        return this.customizedOption;
    }

    @Override
    public DatabaseShard getDatabaseShard(int shardIndex) {
        return databaseShards.get(shardIndex);
    }

    public void setCustomizedOption(DalConfigCustomizedOption customizedOption) {
        this.customizedOption = customizedOption;
    }

    public void setLastLocalizationConfig(LocalizationConfig lastConfig)

    public void addDatabaseShard(DatabaseShard databaseShard) {
        databaseShards.put(databaseShard.getShardIndex(), databaseShard);
    }

    public void setShardStrategy(ShardStrategyProxy shardStrategyProxy) {
        this.shardStrategyProxy = shardStrategyProxy;
    }

    public void setIdGeneratorConfig(ClusterIdGeneratorConfig idGeneratorConfig) {
        this.idGeneratorConfig = idGeneratorConfig;
    }

    public void setRouteStrategyConfig(ClusterRouteStrategyConfig routeStrategyConfig) {
        this.routeStrategyConfig = routeStrategyConfig;
    }

    public void setLocalizationConfig(LocalizationConfig localizationConfig) {
        this.localizationConfig = localizationConfig;
    }

    public void validate() {
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        try {
            return iface.cast(this);
        } catch (ClassCastException e) {
            throw new ClusterRuntimeException(String.format("Unable to unwrap %s to %s", this.toString(), iface.toString()));
        }
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return iface.isInstance(this);
    }
}


package com.ctrip.framework.dal.cluster.client.cluster;

import com.ctrip.framework.dal.cluster.client.Cluster;
import com.ctrip.framework.dal.cluster.client.base.UnsupportedListenable;
import com.ctrip.framework.dal.cluster.client.config.ClusterConfigImpl;
import com.ctrip.framework.dal.cluster.client.config.DalConfigCustomizedOption;
import com.ctrip.framework.dal.cluster.client.config.LocalizationConfig;
import com.ctrip.framework.dal.cluster.client.database.Database;
import com.ctrip.framework.dal.cluster.client.database.DatabaseCategory;
import com.ctrip.framework.dal.cluster.client.exception.ClusterRuntimeException;
import com.ctrip.framework.dal.cluster.client.multihost.ClusterRouteStrategyConfig;
import com.ctrip.framework.dal.cluster.client.shard.DatabaseShard;
import com.ctrip.framework.dal.cluster.client.sharding.context.DbShardContext;
import com.ctrip.framework.dal.cluster.client.sharding.context.TableShardContext;
import com.ctrip.framework.dal.cluster.client.sharding.idgen.ClusterIdGeneratorConfig;
import java.sql.SQLException;
import java.util.*;

/**
 * @author c7ch23en
 */
public class DefaultCluster extends UnsupportedListenable<ClusterSwitchedEvent> implements Cluster {

    private ClusterConfigImpl clusterConfig;

    private Map<Integer, DatabaseShard> databaseShards = new HashMap<>();

    private ShardStrategyProxy shardStrategyProxy;

    private ClusterIdGeneratorConfig idGeneratorConfig;

    private ClusterRouteStrategyConfig routeStrategyConfig;

    private LocalizationConfig localizationConfig;

    private LocalizationConfig lastLocalizationConfig;

    private DalConfigCustomizedOption customizedOption;

    public DefaultCluster(ClusterConfigImpl clusterConfig) {
        this.clusterConfig = clusterConfig;
    }

    @Override
    public String getClusterName() {
        return clusterConfig.getClusterName();
    }

    @Override
    public ClusterType getClusterType() {
        return clusterConfig.getClusterType();
    }

    @Override
    public DatabaseCategory getDatabaseCategory() {
        return clusterConfig.getDatabaseCategory();
    }

    @Override
    public boolean dbShardingEnabled() {
        return databaseShards.size() > 1;
    }

    @Override
    public Integer getDbShard(String tableName, DbShardContext context) {
        return shardStrategyProxy.getDbShard(tableName, context);
    }

    @Override
    public Set<Integer> getAllDbShards() {
        return databaseShards.keySet();
    }

    @Override
    public boolean tableShardingEnabled(String tableName) {
        return shardStrategyProxy.tableShardingEnabled(tableName);
    }

    @Override
    public String getTableShard(String tableName, TableShardContext context) {
        return shardStrategyProxy.getTableShard(tableName, context);
    }

    @Override
    public Set<String> getAllTableShards(String tableName) {
        return shardStrategyProxy.getAllTableShards(tableName);
    }

    @Override
    public String getTableShardSeparator(String tableName) {
        return shardStrategyProxy.getTableShardSeparator(tableName);
    }

    @Override
    public List<Database> getDatabases() {
        List<Database> databases = new LinkedList<>();
        for (DatabaseShard databaseShard : databaseShards.values()) {
            databases.addAll(databaseShard.getMasters());
            databases.addAll(databaseShard.getSlaves());
        }
        return databases;
    }

    @Override
    public Database getMasterOnShard(int shardIndex) {
        List<Database> masters = databaseShards.get(shardIndex).getMasters();
        if (masters == null || masters.isEmpty())
            throw new ClusterRuntimeException("no master available");
        // throw new ClusterRuntimeException("multi masters available");
        return masters.iterator().next();
    }

    @Override
    public List<Database> getSlavesOnShard(int shardIndex) {
        return databaseShards.get(shardIndex).getSlaves();
    }

    @Override
    public ClusterIdGeneratorConfig getIdGeneratorConfig() {
        return idGen

    }

    @Override
    public ClusterRouteStrategyConfig getRouteStrategyConfig() {
        return routeStrategyConfig;
    }

    @Override
    public LocalizationConfig getLocalizationConfig() {
        return localizationConfig;
    }

    @Override
    public LocalizationConfig getLastLocalizationConfig() {
        return this.lastLocalizationConfig;
    }

    @Override
    public DalConfigCustomizedOption getCustomizedOption() {
        return this.customizedOption;
    }

    @Override
    public DatabaseShard getDatabaseShard(int shardIndex) {
        return databaseShards.get(shardIndex);
    }

    public void setCustomizedOption(DalConfigCustomizedOption customizedOption) {
        this.customizedOption = customizedOption;
    }

    public void setLastLocalizationConfig(LocalizationConfig lastConfig) {
        this.lastLocalizationConfig = lastConfig;
    }

    public void addDatabaseShard(DatabaseShard databaseShard) {
        databaseShards.put(databaseShard.getShardIndex(), databaseShard);
    }

    public void setShardStrategy(ShardStrategyProxy shardStrategyProxy) {
        this.shardStrategyProxy = shardStrategyProxy;
    }

    public void setIdGeneratorConfig(ClusterIdGeneratorConfig idGeneratorConfig) {
        this.idGeneratorConfig = idGeneratorConfig;
    }

    public void setRouteStrategyConfig(ClusterRouteStrategyConfig routeStrategyConfig) {
        this.routeStrategyConfig = routeStrategyConfig;
    }

    public void setLocalizationConfig(LocalizationConfig localizationConfig) {
        this.localizationConfig = localizationConfig;
    }

    public void validate() {
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        try {
            return iface.cast(this);
        } catch (ClassCastException e) {
            throw new ClusterRuntimeException(String.format("Unable to unwrap %s to %s", this.toString(), iface.toString()));
        }
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return iface.isInstance(this);
    }
}


try {
            return iface.cast(this);
        } catch (ClassCastException e) {
            throw new ClusterRuntimeException(String.format("Unable to unwrap %s to %s", this.toString(), iface.toString()));
        }

package com.ctrip.framework.dal.cluster.client.cluster;

import com.ctrip.framework.dal.cluster.client.Cluster;
import com.ctrip.framework.dal.cluster.client.base.UnsupportedListenable;
import com.ctrip.framework.dal.cluster.client.config.ClusterConfigImpl;
import com.ctrip.framework.dal.cluster.client.config.DalConfigCustomizedOption;
import com.ctrip.framework.dal.cluster.client.config.LocalizationConfig;
import com.ctrip.framework.dal.cluster.client.database.Database;
import com.ctrip.framework.dal.cluster.client.database.DatabaseCategory;
import com.ctrip.framework.dal.cluster.client.exception.ClusterRuntimeException;
import com.ctrip.framework.dal.cluster.client.multihost.ClusterRouteStrategyConfig;
import com.ctrip.framework.dal.cluster.client.shard.DatabaseShard;
import com.ctrip.framework.dal.cluster.client.sharding.context.DbShardContext;
import com.ctrip.framework.dal.cluster.client.sharding.context.TableShardContext;
import com.ctrip.framework.dal.cluster.client.sharding.idgen.ClusterIdGeneratorConfig;
import java.sql.SQLException;
import java.util.*;

/**
 * @author c7ch23en
 */
public class DefaultCluster extends UnsupportedListenable<ClusterSwitchedEvent> implements Cluster {

    private ClusterConfigImpl clusterConfig;

    private Map<Integer, DatabaseShard> databaseShards = new HashMap<>();

    private ShardStrategyProxy shardStrategyProxy;

    private ClusterIdGeneratorConfig idGeneratorConfig;

    private ClusterRouteStrategyConfig routeStrategyConfig;

    private LocalizationConfig localizationConfig;

    private LocalizationConfig lastLocalizationConfig;

    private DalConfigCustomizedOption customizedOption;

    public DefaultCluster(ClusterConfigImpl clusterConfig) {
        this.clusterConfig = clusterConfig;
    }

    @Override
    public String getClusterName() {
        return clusterConfig.getClusterName();
    }

    @Override
    public ClusterType getClusterType() {
        return clusterConfig.getClusterType();
    }

    @Override
    public DatabaseCategory getDatabaseCategory() {
        return clusterConfig.getDatabaseCategory();
    }

    @Override
    public boolean dbShardingEnabled() {
        return databaseShards.size() > 1;
    }

    @Override
    public Integer getDbShard(String tableName, DbShardContext context) {
        return shardStrategyProxy.getDbShard(tableName, context);
    }

    @Override
    public Set<Integer> getAllDbShards() {
        return databaseShards.keySet();
    }

    @Override
    public boolean tableShardingEnabled(String tableName) {
        return shardStrategyProxy.tableShardingEnabled(tableName);
    }

    @Override
    public String getTableShard(String tableName, TableShardContext context) {
        return shardStrategyProxy.getTableShard(tableName, context);
    }

    @Override
    public Set<String> getAllTableShards(String tableName) {
        return shardStrategyProxy.getAllTableShards(tableName);
    }

    @Override
    public String getTableShardSeparator(String tableName) {
        return shardStrategyProxy.getTableShardSeparator(tableName);
    }

    @Override
    public List<Database> getDatabases() {
        List<Database> databases = new LinkedList<>();
        for (DatabaseShard databaseShard : databaseShards.values()) {
            databases.addAll(databaseShard.getMasters());
            databases.addAll(databaseShard.getSlaves());
        }
        return databases;
    }

    @Override
    public Database getMasterOnShard(int shardIndex) {
        List<Database> masters = databaseShards.get(shardIndex).getMasters();
        if (masters == null || masters.isEmpty())
            throw new ClusterRuntimeException("no master available");
        // throw new ClusterRuntimeException("multi masters available");
        return masters.iterator().next();
    }

    @Override
    public List<Database> getSlavesOnShard(int shardIndex) {
        return databaseShards.get(shardIndex).getSlaves();
    }

    @Override
    public ClusterIdGeneratorConfig getIdGeneratorConfig() {
        return idGeneratorConfig;
    }

    @Override
    public ClusterRouteStrategyConfig getRouteStrategyConfig() {
        return routeStrategyConfig;
    }

    @Override
    public LocalizationConfig getLocalizationConfig() {
        return localizationConfig;
    }

    @Override
    public LocalizationConfig getLastLocalizationConfig() {
        return this.lastLocalizationConfig;
    }

    @Override
    public DalConfigCustomizedOption getCustomizedOption() {
        return this.customizedOption;
    }

    @Override
    public DatabaseShard getDatabaseShard(int shardIndex) {
        return databaseShards.get(shardIndex);
    }

    public void setCustomizedOption(DalConfigCustomizedOption customizedOption) {
        this.customizedOption = customizedOption;
    }

    public void setLastLocalizationConfig(LocalizationConfig lastConfig) {
        this.lastLocalizationConfig = lastConfig;
    }

    public void addDatabaseShard(DatabaseShard databaseShard) {
        databaseShards.put(databaseShard.getShardIndex(), databaseShard);
    }

    public void setShardStrategy(ShardStrategyProxy shardStrategyProxy) {
        this.shardStrategyProxy = shardStrategyProxy;
    }

    public void setIdGeneratorConfig(ClusterIdGeneratorConfig idGeneratorConfig) {
        this.idGeneratorConfig = idGeneratorConfig;
    }

    public void setRouteStrategyConfig(ClusterRouteStrategyConfig routeStrategyConfig) {
        this.routeStrategyConfig = routeStrategyConfig;
    }

    public void setLocalizationConfig(LocalizationConfig localizationConfig) {
        this.localizationConfig = localizationConfig;
    }

    public void validate() {
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {

    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return iface.isInstance(this);
    }
}


package github.nikhilbhutani.firebase;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

/**
 * Created by Nikhil Bhutani on 6/5/2016.
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public class Users {

    private String name;

    public Users() {
    }

{
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}


package github.nikhilbhutani.firebase;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

/**
 * Created by Nikhil Bhutani on 6/5/2016.
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public class Users {

    private String name;

    public Users() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name)


package github.nikhilbhutani.firebase;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

/**
 * Created by Nikhil Bhutani on 6/5/2016.
 */
@JsonIgnoreProperties(ignoreUnknown = true)
public class Users {

    private String name;

    public Users() {
    }

    public String getName() {
        return n

    }

    public void setName(String name) {
        this.name = name;
    }
}


private void removeScheduledTask(Exercise exercise, ExerciseLifecycle lifecycle)

package de.tum.in.www1.artemis.service.scheduled;

import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import de.tum.in.www1.artemis.domain.Exercise;
import de.tum.in.www1.artemis.domain.enumeration.ExerciseLifecycle;
import de.tum.in.www1.artemis.service.ExerciseLifecycleService;
import de.tum.in.www1.artemis.service.util.Tuple;

@Service
public class ScheduleService {

    private final Logger log = LoggerFactory.getLogger(ScheduleService.class);

    private final ExerciseLifecycleService exerciseLifecycleService;

    private final Map<Tuple<Long, ExerciseLifecycle>, Set<ScheduledFuture<?>>> scheduledTasks = new HashMap<>();

    public ScheduleService(ExerciseLifecycleService exerciseLifecycleService) {
        this.exerciseLifecycleService = exerciseLifecycleService;
    }

    private void addScheduledTask(Exercise exercise, ExerciseLifecycle lifecycle, Set<ScheduledFuture<?>> futures) {
        Tuple<Long, ExerciseLifecycle> taskId = new Tuple<>(exercise.getId(), lifecycle);
        scheduledTasks.put(taskId, futures);
    }

{
        removeScheduledTask(exercise.getId(), lifecycle);
    }

    private void removeScheduledTask(Long exerciseId, ExerciseLifecycle lifecycle) {
        Tuple<Long, ExerciseLifecycle> taskId = new Tuple<>(exerciseId, lifecycle);
        scheduledTasks.remove(taskId);
    }

    /**
     * Schedule a task for the given Exercise for the provided ExerciseLifecycle.
     *
     * @param exercise Exercise
     * @param lifecycle ExerciseLifecycle
     * @param task Runnable task to be executed on the lifecycle hook
     */
    void scheduleTask(Exercise exercise, ExerciseLifecycle lifecycle, Runnable task) {
        // no exercise should be scheduled more than once.
        cancelScheduledTaskForLifecycle(exercise.getId(), lifecycle);
        ScheduledFuture<?> scheduledTask = exerciseLifecycleService.scheduleTask(exercise, lifecycle, task);
        addScheduledTask(exercise, lifecycle, Set.of(scheduledTask));
    }

    /**
     * Schedule a set of tasks for the given Exercise for the provided ExerciseLifecycle at the given times.
     *
     * @param exercise Exercise
     * @param lifecycle ExerciseLifecycle
     * @param tasks Runnable tasks to be executed at the associated ZonedDateTimes
     */
    void scheduleTask(Exercise exercise, ExerciseLifecycle lifecycle, Set<Tuple<ZonedDateTime, Runnable>> tasks) {
        // no exercise should be scheduled more than once.
        cancelScheduledTaskForLifecycle(exercise.getId(), lifecycle);
        Set<ScheduledFuture<?>> scheduledTasks = exerciseLifecycleService.scheduleMultipleTasks(exercise, lifecycle, tasks);
        addScheduledTask(exercise, lifecycle, scheduledTasks);
    }

    /**
     * Cancel possible schedules tasks for a provided exercise.
     * @param exerciseId if of the exercise for which a potential scheduled task is canceled
     */
    void cancelScheduledTaskForLifecycle(Long exerciseId, ExerciseLifecycle lifecycle) {
        Tuple<Long, ExerciseLifecycle> taskId = new Tuple<>(exerciseId, lifecycle);
        Set<ScheduledFuture<?>> futures = scheduledTasks.get(taskId);
        if (futures != null) {
            log.debug("Cancelling scheduled task {} for Exercise (#{}).", lifecycle, exerciseId);
            futures.forEach(future -> future.cancel(false));
            removeScheduledTask(exerciseId, lifecycle);
        }
    }
}


{
        Tuple<Long, ExerciseLifecycle> taskId = new Tuple<>(exercise.getId(), lifecycle);
        scheduledTasks.put(taskId, futures);
    }

package de.tum.in.www1.artemis.service.scheduled;

import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import de.tum.in.www1.artemis.domain.Exercise;
import de.tum.in.www1.artemis.domain.enumeration.ExerciseLifecycle;
import de.tum.in.www1.artemis.service.ExerciseLifecycleService;
import de.tum.in.www1.artemis.service.util.Tuple;

@Service
public class ScheduleService {

    private final Logger log = LoggerFactory.getLogger(ScheduleService.class);

    private final ExerciseLifecycleService exerciseLifecycleService;

    private final Map<Tuple<Long, ExerciseLifecycle>, Set<ScheduledFuture<?>>> scheduledTasks = new HashMap<>();

    public ScheduleService(ExerciseLifecycleService exerciseLifecycleService) {
        this.exerciseLifecycleService = exerciseLifecycleService;
    }

    private void addScheduledTask(Exercise exercise, ExerciseLifecycle lifecycle, Set<ScheduledFuture<?>> futures)

    private void removeScheduledTask(Exercise exercise, ExerciseLifecycle lifecycle) {
        removeScheduledTask(exercise.getId(), lifecycle);
    }

    private void removeScheduledTask(Long exerciseId, ExerciseLifecycle lifecycle) {
        Tuple<Long, ExerciseLifecycle> taskId = new Tuple<>(exerciseId, lifecycle);
        scheduledTasks.remove(taskId);
    }

    /**
     * Schedule a task for the given Exercise for the provided ExerciseLifecycle.
     *
     * @param exercise Exercise
     * @param lifecycle ExerciseLifecycle
     * @param task Runnable task to be executed on the lifecycle hook
     */
    void scheduleTask(Exercise exercise, ExerciseLifecycle lifecycle, Runnable task) {
        // no exercise should be scheduled more than once.
        cancelScheduledTaskForLifecycle(exercise.getId(), lifecycle);
        ScheduledFuture<?> scheduledTask = exerciseLifecycleService.scheduleTask(exercise, lifecycle, task);
        addScheduledTask(exercise, lifecycle, Set.of(scheduledTask));
    }

    /**
     * Schedule a set of tasks for the given Exercise for the provided ExerciseLifecycle at the given times.
     *
     * @param exercise Exercise
     * @param lifecycle ExerciseLifecycle
     * @param tasks Runnable tasks to be executed at the associated ZonedDateTimes
     */
    void scheduleTask(Exercise exercise, ExerciseLifecycle lifecycle, Set<Tuple<ZonedDateTime, Runnable>> tasks) {
        // no exercise should be scheduled more than once.
        cancelScheduledTaskForLifecycle(exercise.getId(), lifecycle);
        Set<ScheduledFuture<?>> scheduledTasks = exerciseLifecycleService.scheduleMultipleTasks(exercise, lifecycle, tasks);
        addScheduledTask(exercise, lifecycle, scheduledTasks);
    }

    /**
     * Cancel possible schedules tasks for a provided exercise.
     * @param exerciseId if of the exercise for which a potential scheduled task is canceled
     */
    void cancelScheduledTaskForLifecycle(Long exerciseId, ExerciseLifecycle lifecycle) {
        Tuple<Long, ExerciseLifecycle> taskId = new Tuple<>(exerciseId, lifecycle);
        Set<ScheduledFuture<?>> futures = scheduledTasks.get(taskId);
        if (futures != null) {
            log.debug("Cancelling scheduled task {} for Exercise (#{}).", lifecycle, exerciseId);
            futures.forEach(future -> future.cancel(false));
            removeScheduledTask(exerciseId, lifecycle);
        }
    }
}


package de.tum.in.www1.artemis.service.scheduled;

import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import de.tum.in.www1.artemis.domain.Exercise;
import de.tum.in.www1.artemis.domain.enumeration.ExerciseLifecycle;
import de.tum.in.www1.artemis.service.ExerciseLifecycleService;
import de.tum.in.www1.artemis.service.util.Tuple;

@Service
public class ScheduleService {

    private final Logger log = LoggerFactory.getLogger(ScheduleService.class);

    private final ExerciseLifecycleService exerciseLifecycleService;

    private final Map<Tuple<Long, ExerciseLifecycle>, Set<ScheduledFuture<?>>> scheduledTasks = new HashMap<>();

    public ScheduleService(ExerciseLifecycleService exerciseLifecycleService) {
        this.exerciseLifecycleService = exerciseLifecycleService;
    }

    private void addScheduledTask(Exercise exercise, ExerciseLifecycle lifecycle, Set<ScheduledFuture<?>> futures) {
        Tuple<Long, ExerciseLifecycle> taskId = new Tuple<>(exercise.getId(), lifecycle);
        scheduledTasks.put(taskId, futures);
    }

    private void removeScheduledTask(Exercise exercise, ExerciseLifecycle lifecycle) {
        removeScheduledTask(exercise.getId(), lifecycle);
    }

    private void removeScheduledTask(Long exerciseId, ExerciseLifecycle lifecycle) {
        Tuple<Long, ExerciseLifecycle> taskId = new Tuple<>(exerciseId, lifecycle);
        scheduledTasks.remove(taskId);
    }

    /**
     * Schedule a task for the given Exercise for the provided ExerciseLifecycle.
     *
     * @param exercise Exercise
     * @param lifecycle ExerciseLifecycle
     * @param task Runnable task to be executed on the lifecycle hook
     */
    void scheduleTask(Exercise exercise, ExerciseLifecycle lifecycle, Runnable task) {
        // no exercise should be scheduled more than once.
        cancelScheduledTaskForLifecycle(exercise.getId(), lifecycle);
        ScheduledFuture<?> scheduledTask = exerciseLifecycleService.scheduleTask(exercise, lifecycle, task);
        addScheduledTask(exercise, lifecycle, Set.of(scheduledTask));
    }

    /**
     * Schedule a set of tasks for the given Exercise for the provided ExerciseLifecycle at the given times.
     *
     * @param exercise Exercise
     * @param lifecycle ExerciseLifecycle
     * @param tasks Runnable tasks to be executed at the associated ZonedDateTimes
     */
    void scheduleTask(Exercise exercise, ExerciseLifecycle lifecycle, Set<Tuple<ZonedDateTime, Runnable>> tasks) {
        // no exercise should be scheduled more than once.
        cancelScheduledTaskForLifecycle

        Set<ScheduledFuture<?>> scheduledTasks = exerciseLifecycleService.scheduleMultipleTasks(exercise, lifecycle, tasks);
        addScheduledTask(exercise, lifecycle, scheduledTasks);
    }

    /**
     * Cancel possible schedules tasks for a provided exercise.
     * @param exerciseId if of the exercise for which a potential scheduled task is canceled
     */
    void cancelScheduledTaskForLifecycle(Long exerciseId, ExerciseLifecycle lifecycle) {
        Tuple<Long, ExerciseLifecycle> taskId = new Tuple<>(exerciseId, lifecycle);
        Set<ScheduledFuture<?>> futures = scheduledTasks.get(taskId);
        if (futures != null) {
            log.debug("Cancelling scheduled task {} for Exercise (#{}).", lifecycle, exerciseId);
            futures.forEach(future -> future.cancel(false));
            removeScheduledTask(exerciseId, lifecycle);
        }
    }
}


void scheduleTask(Exercise exercise, ExerciseLifecycle lifecycle, Set<Tuple<ZonedDateTime, Runnable>> tasks) {
        // no exercise should be scheduled more than once.
        cancelScheduledTaskForLifecycle(exercise.getId(), lifecycle);
        Set<ScheduledFuture<?>> scheduledTasks = exerciseLifecycleService.scheduleMultipleTasks(exercise, lifecycle, tasks);
        addScheduledTask(exercise, lifecycle, scheduledTasks);
    }

package de.tum.in.www1.artemis.service.scheduled;

import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import de.tum.in.www1.artemis.domain.Exercise;
import de.tum.in.www1.artemis.domain.enumeration.ExerciseLifecycle;
import de.tum.in.www1.artemis.service.ExerciseLifecycleService;
import de.tum.in.www1.artemis.service.util.Tuple;

@Service
public class ScheduleService {

    private final Logger log = LoggerFactory.getLogger(ScheduleService.class);

    private final ExerciseLifecycleService exerciseLifecycleService;

    private final Map<Tuple<Long, ExerciseLifecycle>, Set<ScheduledFuture<?>>> scheduledTasks = new HashMap<>();

    public ScheduleService(ExerciseLifecycleService exerciseLifecycleService) {
        this.exerciseLifecycleService = exerciseLifecycleService;
    }

    private void addScheduledTask(Exercise exercise, ExerciseLifecycle lifecycle, Set<ScheduledFuture<?>> futures) {
        Tuple<Long, ExerciseLifecycle> taskId = new Tuple<>(exercise.getId(), lifecycle);
        scheduledTasks.put(taskId, futures);
    }

    private void removeScheduledTask(Exercise exercise, ExerciseLifecycle lifecycle) {
        removeScheduledTask(exercise.getId(), lifecycle);
    }

    private void removeScheduledTask(Long exerciseId, ExerciseLifecycle lifecycle) {
        Tuple<Long, ExerciseLifecycle> taskId = new Tuple<>(exerciseId, lifecycle);
        scheduledTasks.remove(taskId);
    }

    /**
     * Schedule a task for the given Exercise for the provided ExerciseLifecycle.
     *
     * @param exercise Exercise
     * @param lifecycle ExerciseLifecycle
     * @param task Runnable task to be executed on the lifecycle hook
     */
    void scheduleTask(Exercise exercise, ExerciseLifecycle lifecycle, Runnable task) {
        // no exercise should be scheduled more than once.
        cancelScheduledTaskForLifecycle(exercise.getId(), lifecycle);
        ScheduledFuture<?> scheduledTask = exerciseLifecycleService.scheduleTask(exercise, lifecycle, task);
        addScheduledTask(exercise, lifecycle, Set.of(scheduledTask));
    }

    /**
     * Schedule a set of tasks for the given Exercise for the provided ExerciseLifecycle at the given times.
     *
     * @param exercise Exercise
     * @param lifecycle ExerciseLifecycle
     * @param tasks Runnable tasks to be executed at the associated ZonedDateTimes
     */

    /**
     * Cancel possible schedules tasks for a provided exercise.
     * @param exerciseId if of the exercise for which a potential scheduled task is canceled
     */
    void cancelScheduledTaskForLifecycle(Long exerciseId, ExerciseLifecycle lifecycle) {
        Tuple<Long, ExerciseLifecycle> taskId = new Tuple<>(exerciseId, lifecycle);
        Set<ScheduledFuture<?>> futures = scheduledTasks.get(taskId);
        if (futures != null) {
            log.debug("Cancelling scheduled task {} for Exercise (#{}).", lifecycle, exerciseId);
            futures.forEach(future -> future.cancel(false));
            removeScheduledTask(exerciseId, lifecycle);
        }
    }
}


@Override
    public boolean isCustomWidth()

/*

{{IS_NOTE
	Purpose:

	Description:

	History:
		2013/12/01 , Created by dennis
}}IS_NOTE

{{IS_RIGHT
}}IS_RIGHT
*/
package org.zkoss.zss.model.impl;

import org.zkoss.zss.model.SBook;
import org.zkoss.zss.model.SCellStyle;
import org.zkoss.zss.model.SColumn;
import org.zkoss.zss.model.SSheet;
import org.zkoss.zss.model.util.Validations;

/**
 * @author dennis
 * @since 3.5.0
 */
public class ColumnArrayImpl extends AbstractColumnArrayAdv {

    private static final long serialVersionUID = 1L;

    private AbstractSheetAdv _sheet;

    private AbstractCellStyleAdv _cellStyle;

    private Integer _width;

    private boolean _hidden = false;

    private boolean _customWidth = false;

    int _index;

    int _lastIndex;

    public ColumnArrayImpl(AbstractSheetAdv sheet, int index, int lastIndex) {
        this._sheet = sheet;
        this._index = index;
        this._lastIndex = lastIndex;
    }

    @Override
    public int getIndex() {
        checkOrphan();
        return _index;
    }

    @Override
    public void checkOrphan() {
        if (_sheet == null) {
            throw new IllegalStateException("doesn't connect to parent");
        }
    }

    @Override
    public void destroy() {
        checkOrphan();
        _sheet = null;
    }

    @Override
    public SSheet getSheet() {
        checkOrphan();
        return _sheet;
    }

    @Override
    public SCellStyle getCellStyle() {
        return getCellStyle(false);
    }

    @Override
    public SCellStyle getCellStyle(boolean local) {
        if (local || _cellStyle != null) {
            return _cellStyle;
        }
        checkOrphan();
        return _sheet.getBook().getDefaultCellStyle();
    }

    @Override
    public void setCellStyle(SCellStyle cellStyle) {
        Validations.argInstance(cellStyle, AbstractCellStyleAdv.class);
        this._cellStyle = (AbstractCellStyleAdv) cellStyle;
    }

    @Override
    public int getWidth() {
        if (_width != null) {
            return _width.intValue();
        }
        checkOrphan();
        return getSheet().getDefaultColumnWidth();
    }

    @Override
    public boolean isHidden() {
        return _hidden;
    }

    @Override
    public void setWidth(int width) {
        this._width = Integer.valueOf(width);
    }

    @Override
    public void setHidden(boolean hidden) {
        if (this._hidden == hidden)
            return;
        this._hidden = hidden;
        // ZSS-988: Subtotal(1xx, range) depends on hidden/unhidden rows/columns
        final SBook book = _sheet.getBook();
        final int row1 = _sheet.getStartRowIndex();
        final int row2 = _sheet.getEndRowIndex();
        final int col1 = getIndex();
        final int col2 = getLastIndex();
        if (row1 >= 0 && row2 >= 0 && col1 >= 0 && col2 >= 0) {
            // ZSS-1047: (side-effect of ZSS-988 and ZSS-1007 which consider setHidden() of SUBTOTAL() function)
            final boolean includePrecedent = false;
            if (!((AbstractBookAdv) book).isPostProcessing()) {
                ModelUpdateUtil.handlePrecedentUpdate(book.getBookSeries(), new RefImpl(book.getBookName(), _sheet.getSheetName(), row1, col1, row2, col2), includePrecedent);
            }
        }
    }

    @Override
    public int getLastIndex() {
        return _lastIndex;
    }

    @Override
    void setIndex(int index) {
        this._index = index;
    }

    @Override
    void setLastIndex(int lastIndex) {
        this._lastIndex = lastIndex;
    }

    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(super.toString()).append("[").append(getIndex()).append("-").append(getLastIndex()).append("]");
        return sb.toString();
    }

{
        return _customWidth || (_width != null && _width != getSheet().getDefaultColumnWidth());
    }

    @Override
    public void setCustomWidth(boolean custom) {
        _customWidth = custom;
    }

    ColumnArrayImpl cloneColumnArrayImpl(AbstractSheetAdv sheet) {
        return cloneColumnArray(sheet, null);
    }

    // ZSS-1132: should skip this Column array in exporting
    public boolean shouldSkip() {
        return !isCustomWidth() && _cellStyle == null && !_hidden;
    }

    ColumnArrayImpl cloneColumnArray(AbstractSheetAdv sheet, SBook book) {
        final ColumnArrayImpl tgt = new ColumnArrayImpl(sheet, this._index, this._lastIndex);
        tgt._width = this._width;
        tgt._hidden = this._hidden;
        tgt._customWidth = this._customWidth;
        tgt._cellStyle = (AbstractCellStyleAdv) (this._cellStyle == null ? null : this._cellStyle.cloneCellStyle(book));
        return tgt;
    }
}


{
        checkOrphan();
        return _index;
    }

/*

{{IS_NOTE
	Purpose:

	Description:

	History:
		2013/12/01 , Created by dennis
}}IS_NOTE

{{IS_RIGHT
}}IS_RIGHT
*/
package org.zkoss.zss.model.impl;

import org.zkoss.zss.model.SBook;
import org.zkoss.zss.model.SCellStyle;
import org.zkoss.zss.model.SColumn;
import org.zkoss.zss.model.SSheet;
import org.zkoss.zss.model.util.Validations;

/**
 * @author dennis
 * @since 3.5.0
 */
public class ColumnArrayImpl extends AbstractColumnArrayAdv {

    private static final long serialVersionUID = 1L;

    private AbstractSheetAdv _sheet;

    private AbstractCellStyleAdv _cellStyle;

    private Integer _width;

    private boolean _hidden = false;

    private boolean _customWidth = false;

    int _index;

    int _lastIndex;

    public ColumnArrayImpl(AbstractSheetAdv sheet, int index, int lastIndex) {
        this._sheet = sheet;
        this._index = index;
        this._lastIndex = lastIndex;
    }

    @Override
    public int getIndex()

    @Override
    public void checkOrphan() {
        if (_sheet == null) {
            throw new IllegalStateException("doesn't connect to parent");
        }
    }

    @Override
    public void destroy() {
        checkOrphan();
        _sheet = null;
    }

    @Override
    public SSheet getSheet() {
        checkOrphan();
        return _sheet;
    }

    @Override
    public SCellStyle getCellStyle() {
        return getCellStyle(false);
    }

    @Override
    public SCellStyle getCellStyle(boolean local) {
        if (local || _cellStyle != null) {
            return _cellStyle;
        }
        checkOrphan();
        return _sheet.getBook().getDefaultCellStyle();
    }

    @Override
    public void setCellStyle(SCellStyle cellStyle) {
        Validations.argInstance(cellStyle, AbstractCellStyleAdv.class);
        this._cellStyle = (AbstractCellStyleAdv) cellStyle;
    }

    @Override
    public int getWidth() {
        if (_width != null) {
            return _width.intValue();
        }
        checkOrphan();
        return getSheet().getDefaultColumnWidth();
    }

    @Override
    public boolean isHidden() {
        return _hidden;
    }

    @Override
    public void setWidth(int width) {
        this._width = Integer.valueOf(width);
    }

    @Override
    public void setHidden(boolean hidden) {
        if (this._hidden == hidden)
            return;
        this._hidden = hidden;
        // ZSS-988: Subtotal(1xx, range) depends on hidden/unhidden rows/columns
        final SBook book = _sheet.getBook();
        final int row1 = _sheet.getStartRowIndex();
        final int row2 = _sheet.getEndRowIndex();
        final int col1 = getIndex();
        final int col2 = getLastIndex();
        if (row1 >= 0 && row2 >= 0 && col1 >= 0 && col2 >= 0) {
            // ZSS-1047: (side-effect of ZSS-988 and ZSS-1007 which consider setHidden() of SUBTOTAL() function)
            final boolean includePrecedent = false;
            if (!((AbstractBookAdv) book).isPostProcessing()) {
                ModelUpdateUtil.handlePrecedentUpdate(book.getBookSeries(), new RefImpl(book.getBookName(), _sheet.getSheetName(), row1, col1, row2, col2), includePrecedent);
            }
        }
    }

    @Override
    public int getLastIndex() {
        return _lastIndex;
    }

    @Override
    void setIndex(int index) {
        this._index = index;
    }

    @Override
    void setLastIndex(int lastIndex) {
        this._lastIndex = lastIndex;
    }

    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(super.toString()).append("[").append(getIndex()).append("-").append(getLastIndex()).append("]");
        return sb.toString();
    }

    @Override
    public boolean isCustomWidth() {
        return _customWidth || (_width != null && _width != getSheet().getDefaultColumnWidth());
    }

    @Override
    public void setCustomWidth(boolean custom) {
        _customWidth = custom;
    }

    ColumnArrayImpl cloneColumnArrayImpl(AbstractSheetAdv sheet) {
        return cloneColumnArray(sheet, null);
    }

    // ZSS-1132: should skip this Column array in exporting
    public boolean shouldSkip() {
        return !isCustomWidth() && _cellStyle == null && !_hidden;
    }

    ColumnArrayImpl cloneColumnArray(AbstractSheetAdv sheet, SBook book) {
        final ColumnArrayImpl tgt = new ColumnArrayImpl(sheet, this._index, this._lastIndex);
        tgt._width = this._width;
        tgt._hidden = this._hidden;
        tgt._customWidth = this._customWidth;
        tgt._cellStyle = (AbstractCellStyleAdv) (this._cellStyle == null ? null : this._cellStyle.cloneCellStyle(book));
        return tgt;
    }
}


/*

{{IS_NOTE
	Purpose:

	Description:

	History:
		2013/12/01 , Created by dennis
}}IS_NOTE

{{IS_RIGHT
}}IS_RIGHT
*/
package org.zkoss.zss.model.impl;

import org.zkoss.zss.model.SBook;
import org.zkoss.zss.model.SCellStyle;
import org.zkoss.zss.model.SColumn;
import org.zkoss.zss.model.SSheet;
import org.zkoss.zss.model.util.Validations;

/**
 * @author dennis
 * @since 3.5.0
 */
public class ColumnArrayImpl extends AbstractColumnArrayAdv {

    private static final long serialVersionUID = 1L;

    private AbstractSheetAdv _sheet;

    private AbstractCellStyleAdv _cellStyle;

    private Integer _width;

    private boolean _hidden = false;

    private boolean _customWidth = false;

    int _index;

    int _lastIndex;

    public ColumnArrayImpl(AbstractSheetAdv sheet, int index, int lastIndex) {
        this._sheet = sheet;
        this._index = index;
        this._lastIndex = lastIndex;
    }

    @Override
    public int getIndex() {
        checkOrphan();
        return _index;
    }

    @Override
    public void checkOrphan() {
        if (_sheet == null) {
            throw new IllegalStateException("doesn't connect to parent");
        }
    }

    @Override
    public void destroy() {
        checkOrphan();
        _sheet = null;
    }

    @Override
    public SSheet getSheet() {
        checkOrphan();
        return _sheet;
    }

    @Override
    public SCellStyle getCellStyle() {
        return getCellStyle(false);
    }

    @Override
    public SCellStyle getCellStyle(boolean local) {
        if (local || _cellStyle != null) {
            return _cellStyle;
        }
        checkOrphan();
        r

    }

    @Override
    public void setCellStyle(SCellStyle cellStyle) {
        Validations.argInstance(cellStyle, AbstractCellStyleAdv.class);
        this._cellStyle = (AbstractCellStyleAdv) cellStyle;
    }

    @Override
    public int getWidth() {
        if (_width != null) {
            return _width.intValue();
        }
        checkOrphan();
        return getSheet().getDefaultColumnWidth();
    }

    @Override
    public boolean isHidden() {
        return _hidden;
    }

    @Override
    public void setWidth(int width) {
        this._width = Integer.valueOf(width);
    }

    @Override
    public void setHidden(boolean hidden) {
        if (this._hidden == hidden)
            return;
        this._hidden = hidden;
        // ZSS-988: Subtotal(1xx, range) depends on hidden/unhidden rows/columns
        final SBook book = _sheet.getBook();
        final int row1 = _sheet.getStartRowIndex();
        final int row2 = _sheet.getEndRowIndex();
        final int col1 = getIndex();
        final int col2 = getLastIndex();
        if (row1 >= 0 && row2 >= 0 && col1 >= 0 && col2 >= 0) {
            // ZSS-1047: (side-effect of ZSS-988 and ZSS-1007 which consider setHidden() of SUBTOTAL() function)
            final boolean includePrecedent = false;
            if (!((AbstractBookAdv) book).isPostProcessing()) {
                ModelUpdateUtil.handlePrecedentUpdate(book.getBookSeries(), new RefImpl(book.getBookName(), _sheet.getSheetName(), row1, col1, row2, col2), includePrecedent);
            }
        }
    }

    @Override
    public int getLastIndex() {
        return _lastIndex;
    }

    @Override
    void setIndex(int index) {
        this._index = index;
    }

    @Override
    void setLastIndex(int lastIndex) {
        this._lastIndex = lastIndex;
    }

    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(super.toString()).append("[").append(getIndex()).append("-").append(getLastIndex()).append("]");
        return sb.toString();
    }

    @Override
    public boolean isCustomWidth() {
        return _customWidth || (_width != null && _width != getSheet().getDefaultColumnWidth());
    }

    @Override
    public void setCustomWidth(boolean custom) {
        _customWidth = custom;
    }

    ColumnArrayImpl cloneColumnArrayImpl(AbstractSheetAdv sheet) {
        return cloneColumnArray(sheet, null);
    }

    // ZSS-1132: should skip this Column array in exporting
    public boolean shouldSkip() {
        return !isCustomWidth() && _cellStyle == null && !_hidden;
    }

    ColumnArrayImpl cloneColumnArray(AbstractSheetAdv sheet, SBook book) {
        final ColumnArrayImpl tgt = new ColumnArrayImpl(sheet, this._index, this._lastIndex);
        tgt._width = this._width;
        tgt._hidden = this._hidden;
        tgt._customWidth = this._customWidth;
        tgt._cellStyle = (AbstractCellStyleAdv) (this._cellStyle == null ? null : this._cellStyle.cloneCellStyle(book));
        return tgt;
    }
}


public void removePlotDataType(int index)

package net.sf.openrocket.gui.plot;

import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.simulation.FlightDataBranch;
import net.sf.openrocket.simulation.FlightDataType;
import net.sf.openrocket.simulation.FlightEvent;
import net.sf.openrocket.startup.Application;
import net.sf.openrocket.unit.Unit;
import net.sf.openrocket.util.ArrayList;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.MathUtil;
import net.sf.openrocket.util.Pair;

public class PlotConfiguration implements Cloneable {

    private static final Translator trans = Application.getTranslator();

    public static final PlotConfiguration[] DEFAULT_CONFIGURATIONS;

    static {
        ArrayList<PlotConfiguration> configs = new ArrayList<PlotConfiguration>();
        PlotConfiguration config;
        // // Vertical motion vs. time
        config = new PlotConfiguration(trans.get("PlotConfiguration.Verticalmotion"));
        config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 0);
        config.addPlotDataType(FlightDataType.TYPE_VELOCITY_Z);
        config.addPlotDataType(FlightDataType.TYPE_ACCELERATION_Z);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        config.setEvent(FlightEvent.Type.TUMBLE, true);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        // // Total motion vs. time
        config = new PlotConfiguration(trans.get("PlotConfiguration.Totalmotion"));
        config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 0);
        config.addPlotDataType(FlightDataType.TYPE_VELOCITY_TOTAL);
        config.addPlotDataType(FlightDataType.TYPE_ACCELERATION_TOTAL);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        config.setEvent(FlightEvent.Type.TUMBLE, true);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        // // Flight side profile
        config = new PlotConfiguration(trans.get("PlotConfiguration.Flightside"), FlightDataType.TYPE_POSITION_X);
        config.addPlotDataType(FlightDataType.TYPE_ALTITUDE);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        config.setEvent(FlightEvent.Type.TUMBLE, true);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        config = new PlotConfiguration(trans.get("PlotConfiguration.Groundtrack"), FlightDataType.TYPE_POSITION_X);
        config.addPlotDataType(FlightDataType.TYPE_POSITION_Y, 0);
        config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 1);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        configs.add(config);
        config = new PlotConfiguration(trans.get("PlotConfiguration.Stability"));
        config.addPlotDataType(FlightDataType.TYPE_STABILITY, 0);
        config.addPlotDataType(FlightDataType.TYPE_CP_LOCATION, 1);
        config.addPlotDataType(FlightDataType.TYPE_CG_LOCATION, 1);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        config.setEvent(FlightEvent.Type.TUMBLE, true);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        // // Drag coefficients vs. Mach number
        config = new PlotConfiguration(trans.get("PlotConfiguration.Dragcoef"), FlightDataType.TYPE_MACH_NUMBER);
        config.addPlotDataType(FlightDataType.TYPE_DRAG_COEFF, 0);
        config.addPlotDataType(FlightDataType.TYPE_FRICTION_DRAG_COEFF, 0);
        config.addPlotDataType(FlightDataType.TYPE_BASE_DRAG_COEFF, 0);
        config.addPlotDataType(FlightDataType.TYPE_PRESSURE_DRAG_COEFF, 0);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        config = new PlotConfiguration(trans.get("PlotConfiguration.Rollcharacteristics"));
        config.addPlotDataType(FlightDataType.TYPE_ROLL_RATE, 0);
        config.addPlotDataType(FlightDataType.TYPE_ROLL_MOMENT_COEFF, 1);
        config.addPlotDataType(FlightDataType.TYPE_ROLL_FORCING_COEFF, 1);
        config.addPlotDataType(FlightDataType.TYPE_ROLL_DAMPING_COEFF, 1);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.LAUNCHROD, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        config.setEvent(FlightEvent.Type.TUMBLE, true);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        // // Angle of attack and orientation vs. time
        config = new PlotConfiguration(trans.get("PlotConfiguration.Angleofattack"));
        config.addPlotDataType(FlightDataType.TYPE_AOA, 0);
        config.addPlotDataType(FlightDataType.TYPE_ORIENTATION_PHI);
        config.addPlotDataType(FlightDataType.TYPE_ORIENTATION_THETA);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        config.setEvent(FlightEvent.Type.TUMBLE, true);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        // // Simulation time step and computation time
        config = new PlotConfiguration(trans.get("PlotConfiguration.Simulationtime"));
        config.addPlotDataType(FlightDataType.TYPE_TIME_STEP);
        config.addPlotDataType(FlightDataType.TYPE_COMPUTATION_TIME);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        config.setEvent(FlightEvent.Type.TUMBLE, true);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        DEFAULT_CONFIGURATIONS = configs.toArray(new PlotConfiguration[0]);
    }

    /**
     * Bonus given for the first type being on the first axis
     */
    private static final double BONUS_FIRST_TYPE_ON_FIRST_AXIS = 1.0;

    /**
     * Bonus given if the first axis includes zero (to prefer first axis having zero over
     * the others)
     */
    private static final double BONUS_FIRST_AXIS_HAS_ZERO = 2.0;

    /**
     * Bonus given for a common zero point on left and right axes.
     */
    private static final double BONUS_COMMON_ZERO = 40.0;

    /**
     * Bonus given for only using a single axis.
     */
    private static final double BONUS_ONLY_ONE_AXIS = 50.0;

    // 30% of total range
    private static final double INCLUDE_ZERO_DISTANCE = 0.3;

    /**
     * The data types to be plotted.
     */
    private ArrayList<FlightDataType> plotDataTypes = new ArrayList<FlightDataType>();

    private ArrayList<Unit> plotDataUnits = new ArrayList<Unit>();

    /**
     * The corresponding Axis on which they will be plotted, or null to auto-select.
     */
    private ArrayList<Integer> plotDataAxes = new ArrayList<Integer>();

    private EnumSet<FlightEvent.Type> events = EnumSet.noneOf(FlightEvent.Type.class);

    private FlightDataType domainAxisType = null;

    private Unit domainAxisUnit = null;

    /**
     * All available axes.
     */
    private final int axesCount;

    private ArrayList<Axis> allAxes = new ArrayList<Axis>();

    private String name = null;

    public PlotConfiguration() {
        this(null, FlightDataType.TYPE_TIME);
    }

    public PlotConfiguration(String name) {
        this(name, FlightDataType.TYPE_TIME);
    }

    public PlotConfiguration(String name, FlightDataType domainType) {
        this.name = name;
        // Two axes
        allAxes.add(new Axis());
        allAxes.add(new Axis());
        axesCount = 2;
        setDomainAxisType(domainType);
    }

    public FlightDataType getDomainAxisType() {
        return domainAxisType;
    }

    public void setDomainAxisType(FlightDataType type) {
        boolean setUnit;
        if (domainAxisType != null && domainAxisType.getUnitGroup() == type.getUnitGroup())
            setUnit = false;
        else
            setUnit = true;
        domainAxisType = type;
        if (setUnit)
            domainAxisUnit = domainAxisType.getUnitGroup().getDefaultUnit();
    }

    public Unit getDomainAxisUnit() {
        return domainAxisUnit;
    }

    public void setDomainAxisUnit(Unit u) {
        if (!domainAxisType.getUnitGroup().contains(u)) {
            throw new IllegalArgumentException("Setting unit " + u + " to type " + domainAxisType);
        }
        domainAxisUnit = u;
    }

    public void addPlotDataType(FlightDataType type) {
        plotDataTypes.add(type);
        plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
        plotDataAxes.add(-1);
    }

    public void addPlotDataType(FlightDataType type, int axis) {
        if (axis >= axesCount) {
            throw new IllegalArgumentException("Axis index too large");
        }
        plotDataTypes.add(type);
        plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
        plotDataAxes.add(axis);
    }

    public void setPlotDataType(int index, FlightDataType type) {
        FlightDataType origType = plotDataTypes.get(index);
        plotDataTypes.set(index, type);
        if (origType.getUnitGroup() != type.getUnitGroup()) {
            plotDataUnits.set(index, type.getUnitGroup().getDefaultUnit());
        }
    }

    public void setPlotDataUnit(int index, Unit unit) {
        if (!plotDataTypes.get(index).getUnitGroup().contains(unit)) {
            throw new IllegalArgumentException("Attempting to set unit " + unit + " to group " + plotDataTypes.get(index).getUnitGroup());
        }
        plotDataUnits.set(index, unit);
    }

    public void setPlotDataAxis(int index, int axis) {
        if (axis >= axesCount) {
            throw new IllegalArgumentException("Axis index too large");
        }
        plotDataAxes.set(index, axis);
    }

    public void setPlotDataType(int index, FlightDataType type, Unit unit, int axis) {
        if (axis >= axesCount) {
            throw new IllegalArgumentException("Axis index too large");
        }
        plotDataTypes.set(index, type);
        plotDataUnits.set(index, unit);
        plotDataAxes.set(index, axis);
    }

{
        plotDataTypes.remove(index);
        plotDataUnits.remove(index);
        plotDataAxes.remove(index);
    }

    public FlightDataType getType(int index) {
        return plotDataTypes.get(index);
    }

    public Unit getUnit(int index) {
        return plotDataUnits.get(index);
    }

    public int getAxis(int index) {
        return plotDataAxes.get(index);
    }

    public int getTypeCount() {
        return plotDataTypes.size();
    }

    public Set<FlightEvent.Type> getActiveEvents() {
        return events.clone();
    }

    public void setEvent(FlightEvent.Type type, boolean active) {
        if (active) {
            events.add(type);
        } else {
            events.remove(type);
        }
    }

    public boolean isEventActive(FlightEvent.Type type) {
        return events.contains(type);
    }

    public List<Axis> getAllAxes() {
        List<Axis> list = new ArrayList<Axis>();
        list.addAll(allAxes);
        return list;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * Returns the name of this PlotConfiguration.
     */
    @Override
    public String toString() {
        return name;
    }

    /**
     * Find the best combination of the auto-selectable axes.
     *
     * @return	a new PlotConfiguration with the best fitting auto-selected axes and
     * 			axes ranges selected.
     */
    public PlotConfiguration fillAutoAxes(FlightDataBranch data) {
        PlotConfiguration config = recursiveFillAutoAxes(data).getU();
        config.fitAxes(data);
        return config;
    }

    /**
     * Recursively search for the best combination of the auto-selectable axes.
     * This is a brute-force search method.
     *
     * @return	a new PlotConfiguration with the best fitting auto-selected axes and
     * 			axes ranges selected, and the goodness value
     */
    private Pair<PlotConfiguration, Double> recursiveFillAutoAxes(FlightDataBranch data) {
        // Create copy to fill in
        PlotConfiguration copy = this.clone();
        int autoindex;
        for (autoindex = 0; autoindex < plotDataAxes.size(); autoindex++) {
            if (plotDataAxes.get(autoindex) < 0)
                break;
        }
        if (autoindex >= plotDataAxes.size()) {
            // All axes have been assigned, just return since we are already the best
            return new Pair<PlotConfiguration, Double>(copy, copy.getGoodnessValue(data));
        }
        // Set the auto-selected index one at a time and choose the best one
        PlotConfiguration best = null;
        double bestValue = Double.NEGATIVE_INFINITY;
        for (int i = 0; i < axesCount; i++) {
            copy.plotDataAxes.set(autoindex, i);
            Pair<PlotConfiguration, Double> result = copy.recursiveFillAutoAxes(data);
            if (result.getV() > bestValue) {
                best = result.getU();
                bestValue = result.getV();
            }
        }
        return new Pair<PlotConfiguration, Double>(best, bestValue);
    }

    /**
     * Fit the axes to hold the provided data.  All of the plotDataAxis elements must
     * be non-negative.
     *

     * NOTE: This method assumes that only two axes are used.
     */
    protected void fitAxes(FlightDataBranch data) {
        // Reset axes
        for (Axis a : allAxes) {
            a.reset();
        }
        // Add full range to the axes
        int length = plotDataTypes.size();
        for (int i = 0; i < length; i++) {
            FlightDataType type = plotDataTypes.get(i);
            Unit unit = plotDataUnits.get(i);
            int index = plotDataAxes.get(i);
            if (index < 0) {
                throw new IllegalStateException("fitAxes called with auto-selected axis");
            }
            Axis axis = allAxes.get(index);
            double min = unit.toUnit(data.getMinimum(type));
            double max = unit.toUnit(data.getMaximum(type));
            axis.addBound(min);
            axis.addBound(max);
        }
        // Ensure non-zero (or NaN) range, add a few percent range, include zero if it is close
        for (Axis a : allAxes) {
            if (MathUtil.equals(a.getMinValue(), a.getMaxValue())) {
                a.addBound(a.getMinValue() - 1);
                a.addBound(a.getMaxValue() + 1);
            }
            double addition = a.getRangeLength() * 0.03;
            a.addBound(a.getMinValue() - addition);
            a.addBound(a.getMaxValue() + addition);
            double dist;
            dist = Math.min(Math.abs(a.getMinValue()), Math.abs(a.getMaxValue()));
            if (dist <= a.getRangeLength() * INCLUDE_ZERO_DISTANCE) {
                a.addBound(0);
            }
        }
        // Check whether to use a common zero
        Axis left = allAxes.get(0);
        Axis right = allAxes.get(1);
        if (left.getMinValue() > 0 || left.getMaxValue() < 0 || right.getMinValue() > 0 || right.getMaxValue() < 0 || Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
            return;
        // // Compute common zero
        // TODO: MEDIUM: This algorithm may require tweaking
        double min1 = left.getMinValue();
        double max1 = left.getMaxValue();
        double min2 = right.getMinValue();
        double max2 = right.getMaxValue();
        // Calculate and round scaling factor
        double scale = Math.max(left.getRangeLength(), right.getRangeLength()) / Math.min(left.getRangeLength(), right.getRangeLength());
        scale = roundScale(scale);
        if (right.getRangeLength() > left.getRangeLength()) {
            scale = 1 / scale;
        }
        // Scale right axis, enlarge axes if necessary and scale back
        min2 *= scale;
        max2 *= scale;
        min1 = Math.min(min1, min2);
        min2 = min1;
        max1 = Math.max(max1, max2);
        max2 = max1;
        min2 /= scale;
        max2 /= scale;
        // Apply scale
        left.addBound(min1);
        left.addBound(max1);
        right.addBound(min2);
        right.addBound(max2);
    }

    private double roundScale(double scale) {
        double mul = 1;
        while (scale >= 10) {
            scale /= 10;
            mul *= 10;
        }
        while (scale < 1) {
            scale *= 10;
            mul /= 10;
        }
        if (scale > 7.5) {
            scale = 10;
        } else if (scale > 4.5) {
            scale = 5;
        } else if (scale > 3) {
            scale = 4;
        } else if (scale > 1.5) {
            scale = 2;
        } else {
            scale = 1;
        }
        return scale * mul;
    }

    @SuppressWarnings("unused")
    private double roundScaleUp(double scale) {
        double mul = 1;
        while (scale >= 10) {
            scale /= 10;
            mul *= 10;
        }
        while (scale < 1) {
            scale *= 10;
            mul /= 10;
        }
        if (scale > 5) {
            scale = 10;
        } else if (scale > 4) {
            scale = 5;
        } else if (scale > 2) {
            scale = 4;
        } else if (scale > 1) {
            scale = 2;
        } else {
            scale = 1;
        }
        return scale * mul;
    }

    @SuppressWarnings("unused")
    private double roundScaleDown(double scale) {
        double mul = 1;
        while (scale >= 10) {
            scale /= 10;
            mul *= 10;
        }
        while (scale < 1) {
            scale *= 10;
            mul /= 10;
        }
        if (scale > 5) {
            scale = 5;
        } else if (scale > 4) {
            scale = 4;
        } else if (scale > 2) {
            scale = 2;
        } else {
            scale = 1;
        }
        return scale * mul;
    }

    /**
     * Fits the axis ranges to the data and returns the "goodness value" of this
     * selection of axes.  All plotDataAxis elements must be non-null.
     *

     * NOTE: This method assumes that all data can fit into the axes ranges and
     * that only two axes are used.
     *
     * @return	a "goodness value", the larger the better.
     */
    protected double getGoodnessValue(FlightDataBranch data) {
        double goodness = 0;
        int length = plotDataTypes.size();
        // Fit the axes ranges to the data
        fitAxes(data);
        /*
		 * Calculate goodness of ranges.  100 points is given if the values fill the
		 * entire range, 0 if they fill none of it.
		 */
        for (int i = 0; i < length; i++) {
            FlightDataType type = plotDataTypes.get(i);
            Unit unit = plotDataUnits.get(i);
            int index = plotDataAxes.get(i);
            if (index < 0) {
                throw new IllegalStateException("getGoodnessValue called with auto-selected axis");
            }
            Axis axis = allAxes.get(index);
            double min = unit.toUnit(data.getMinimum(type));
            double max = unit.toUnit(data.getMaximum(type));
            if (Double.isNaN(min) || Double.isNaN(max))
                continue;
            if (MathUtil.equals(min, max))
                continue;
            double d = (max - min) / axis.getRangeLength();
            // Prioritize small ranges
            d = MathUtil.safeSqrt(d);
            goodness += d * 100.0;
        }
        /*
		 * Add extra points for specific things.
		 */
        // A little for the first type being on the first axis
        if (plotDataAxes.get(0) == 0)
            goodness += BONUS_FIRST_TYPE_ON_FIRST_AXIS;
        // A little bonus if the first axis contains zero
        Axis left = allAxes.get(0);
        if (left.getMinValue() <= 0 && left.getMaxValue() >= 0)
            goodness += BONUS_FIRST_AXIS_HAS_ZERO;
        // A boost if a common zero was used in the ranging
        Axis right = allAxes.get(1);
        if (left.getMinValue() <= 0 && left.getMaxValue() >= 0 && right.getMinValue() <= 0 && right.getMaxValue() >= 0)
            goodness += BONUS_COMMON_ZERO;
        // A boost if only one axis is used
        if (Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
            goodness += BONUS_ONLY_ONE_AXIS;
        return goodness;
    }

    /**
     * Reset the units of this configuration to the default units. Returns this
     * PlotConfiguration.
     *
     * @return   this PlotConfiguration.
     */
    public PlotConfiguration resetUnits() {
        for (int i = 0; i < plotDataTypes.size(); i++) {
            plotDataUnits.set(i, plotDataTypes.get(i).getUnitGroup().getDefaultUnit());
        }
        return this;
    }

    @Override
    public PlotConfiguration clone() {
        try {
            PlotConfiguration copy = (PlotConfiguration) super.clone();
            // Shallow-clone all immutable lists
            copy.plotDataTypes = this.plotDataTypes.clone();
            copy.plotDataAxes = this.plotDataAxes.clone();
            copy.plotDataUnits = this.plotDataUnits.clone();
            copy.events = this.events.clone();
            // Deep-clone all Axis since they are mutable
            copy.allAxes = new ArrayList<Axis>();
            for (Axis a : this.allAxes) {
                copy.allAxes.add(a.clone());
            }
            return copy;
        } catch (CloneNotSupportedException e) {
            throw new BugException("BUG! Could not clone().");
        }
    }
}


package net.sf.openrocket.gui.plot;

import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.simulation.FlightDataBranch;
import net.sf.openrocket.simulation.FlightDataType;
import net.sf.openrocket.simulation.FlightEvent;
import net.sf.openrocket.startup.Application;
import net.sf.openrocket.unit.Unit;
import net.sf.openrocket.util.ArrayList;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.MathUtil;
import net.sf.openrocket.util.Pair;

public class PlotConfiguration implements Cloneable {

    private static final Translator trans = Application.getTranslator();

    public static final PlotConfiguration[] DEFAULT_CONFIGURATIONS;

    static {
        ArrayList<PlotConfiguration> configs = new ArrayList<PlotConfiguration>();
        PlotConfiguration config;
        // // Vertical motion vs. time
        config = new PlotConfiguration(trans.get("PlotConfiguration.Verticalmotion"));
        config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 0);
        config.addPlotDataType(FlightDataType.TYPE_VELOCITY_Z);
        config.addPlotDataType(FlightDataType.TYPE_ACCELERATION_Z);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        config.setEvent(FlightEvent.Type.TUMBLE, true);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        // // Total motion vs. time
        config = new PlotConfiguration(trans.get("PlotConfiguration.Totalmotion"));
        config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 0);
        config.addPlotDataType(FlightDataType.TYPE_VELOCITY_TOTAL);
        config.addPlotDataType(FlightDataType.TYPE_ACCELERATION_TOTAL);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        config.setEvent(FlightEvent.Type.TUMBLE, true);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        // // Flight side profile
        config = new PlotConfiguration(trans.get("PlotConfiguration.Flightside"), FlightDataType.TYPE_POSITION_X);
        config.addPlotDataType(FlightDataType.TYPE_ALTITUDE);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        config.setEvent(FlightEvent.Type.TUMBLE, true);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        config = new PlotConfiguration(trans.get("PlotConfiguration.Groundtrack"), FlightDataType.TYPE_POSITION_X);
        config.addPlotDataType(FlightDataType.TYPE_POSITION_Y, 0);
        config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 1);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        configs.add(config);
        config = new PlotConfiguration(trans.get("PlotConfiguration.Stability"));
        config.addPlotDataType(FlightDataType.TYPE_STABILITY, 0);
        config.addPlotDataType(FlightDataType.TYPE_CP_LOCATION, 1);
        config.addPlotDataType(FlightDataType.TYPE_CG_LOCATION, 1);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        config.setEvent(FlightEvent.Type.TUMBLE, true);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        // // Drag coefficients vs. Mach number
        config = new PlotConfiguration(trans.get("PlotConfiguration.Dragcoef"), FlightDataType.TYPE_MACH_NUMBER);
        config.addPlotDataType(FlightDataType.TYPE_DRAG_COEFF, 0);
        config.addPlotDataType(FlightDataType.TYPE_FRICTION_DRAG_COEFF, 0);
        config.addPlotDataType(FlightDataType.TYPE_BASE_DRAG_COEFF, 0);
        config.addPlotDataType(FlightDataType.TYPE_PRESSURE_DRAG_COEFF, 0);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        config = new PlotConfiguration(trans.get("PlotConfiguration.Rollcharacteristics"));
        config.addPlotDataType(FlightDataType.TYPE_ROLL_RATE, 0);
        config.addPlotDataType(FlightDataType.TYPE_ROLL_MOMENT_COEFF, 1);
        config.addPlotDataType(FlightDataType.TYPE_ROLL_FORCING_COEFF, 1);
        config.addPlotDataType(FlightDataType.TYPE_ROLL_DAMPING_COEFF, 1);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.LAUNCHROD, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        config.setEvent(FlightEvent.Type.TUMBLE, true);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        // // Angle of attack and orientation vs. time
        config = new PlotConfiguration(trans.get("PlotConfiguration.Angleofattack"));
        config.addPlotDataType(FlightDataType.TYPE_AOA, 0);
        config.addPlotDataType(FlightDataType.TYPE_ORIENTATION_PHI);
        config.addPlotDataType(FlightDataType.TYPE_ORIENTATION_THETA);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        config.setEvent(FlightEvent.Type.TUMBLE, true);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        // // Simulation time step and computation time
        config = new PlotConfiguration(trans.get("PlotConfiguration.Simulationtime"));
        config.addPlotDataType(FlightDataType.TYPE_TIME_STEP);
        config.addPlotDataType(FlightDataType.TYPE_COMPUTATION_TIME);
        config.setEvent(FlightEvent.Type.IGNITION, true);
        config.setEvent(FlightEvent.Type.BURNOUT, true);
        config.setEvent(FlightEvent.Type.APOGEE, true);
        config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true);
        config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true);
        config.setEvent(FlightEvent.Type.GROUND_HIT, true);
        config.setEvent(FlightEvent.Type.TUMBLE, true);
        config.setEvent(FlightEvent.Type.EXCEPTION, true);
        configs.add(config);
        DEFAULT_CONFIGURATIONS = configs.toArray(new PlotConfiguration[0]);
    }

    /**
     * Bonus given for the first type being on the first axis
     */
    private static final double BONUS_FIRST_TYPE_ON_FIRST_AXIS = 1.0;

    /**
     * Bonus given if the first axis includes zero (to prefer first axis having zero over
     * the others)
     */
    private static final double BONUS_FIRST_AXIS_HAS_ZERO = 2.0;

    /**
     * Bonus given for a common zero point on left and right axes.
     */
    private static final double BONUS_COMMON_ZERO = 40.0;

    /**
     * Bonus given for only using a single axis.
     */
    private static final double BONUS_ONLY_ONE_AXIS = 50.0;

    // 30% of total range
    private static final double INCLUDE_ZERO_DISTANCE = 0.3;

    /**
     * The data types to be plotted.
     */
    private ArrayList<FlightDataType> plotDataTypes = new ArrayList<FlightDataType>();

    private ArrayList<Unit> plotDataUnits = new ArrayList<Unit>();

    /**
     * The corresponding Axis on which they will be plotted, or null to auto-select.
     */
    private ArrayList<Integer> plotDataAxes = new ArrayList<Integer>();

    private EnumSet<FlightEvent.Type> events = EnumSet.noneOf(FlightEvent.Type.class);

    private FlightDataType domainAxisType = null;

    private Unit domainAxisUnit = null;

    /**
     * All available axes.
     */
    private final int axesCount;

    private ArrayList<Axis> allAxes = new ArrayList<Axis>();

    private String name = null;

    public PlotConfiguration() {
        this(null, FlightDataType.TYPE_TIME);
    }

    public PlotConfiguration(String name) {
        this(name, FlightDataType.TYPE_TIME);
    }

    public PlotConfiguration(String name, FlightDataType domainType) {
        this.name = name;
        // Two axes
        allAxes.add(new Axis());
        allAxes.add(new Axis());
        axesCount = 2;
        setDomainAxisType(domainType);
    }

    public FlightDataType getDomainAxisType()

public void setDomainAxisType(FlightDataType type) {
        boolean setUnit;
        if (domainAxisType != null && domainAxisType.getUnitGroup() == type.getUnitGroup())
            setUnit = false;
        else
            setUnit = true;
        domainAxisType = type;
        if (setUnit)
            domainAxisUnit = domainAxisType.getUnitGroup().getDefaultUnit();
    }

    public Unit getDomainAxisUnit() {
        return domainAxisUnit;
    }

    public void setDomainAxisUnit(Unit u) {
        if (!domainAxisType.getUnitGroup().contains(u)) {
            throw new IllegalArgumentException("Setting unit " + u + " to type " + domainAxisType);
        }
        domainAxisUnit = u;
    }

    public void addPlotDataType(FlightDataType type) {
        plotDataTypes.add(type);
        plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
        plotDataAxes.add(-1);
    }

    public void addPlotDataType(FlightDataType type, int axis) {
        if (axis >= axesCount) {
            throw new IllegalArgumentException("Axis index too large");
        }
        plotDataTypes.add(type);
        plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
        plotDataAxes.add(axis);
    }

    public void setPlotDataType(int index, FlightDataType type) {
        FlightDataType origType = plotDataTypes.get(index);
        plotDataTypes.set(index, type);
        if (origType.getUnitGroup() != type.getUnitGroup()) {
            plotDataUnits.set(index, type.getUnitGroup().getDefaultUnit());
        }
    }

    public void setPlotDataUnit(int index, Unit unit) {
        if (!plotDataTypes.get(index).getUnitGroup().contains(unit)) {
            throw new IllegalArgumentException("Attempting to set unit " + unit + " to group " + plotDataTypes.get(index).getUnitGroup());
        }
        plotDataUnits.set(index, unit);
    }

    public void setPlotDataAxis(int index, int axis) {
        if (axis >= axesCount) {
            throw new IllegalArgumentException("Axis index too large");
        }
        plotDataAxes.set(index, axis);
    }

    public void setPlotDataType(int index, FlightDataType type, Unit unit, int axis) {
        if (axis >= axesCount) {
            throw new IllegalArgumentException("Axis index too large");
        }
        plotDataTypes.set(index, type);
        plotDataUnits.set(index, unit);
        plotDataAxes.set(index, axis);
    }

    public void removePlotDataType(int index) {
        plotDataTypes.remove(index);
        plotDataUnits.remove(index);
        plotDataAxes.remove(index);
    }

    public FlightDataType getType(int index) {
        return plotDataTypes.get(index);
    }

    public Unit getUnit(int index) {
        return plotDataUnits.get(index);
    }

    public int getAxis(int index) {
        return plotDataAxes.get(index);
    }

    public int getTypeCount() {
        return plotDataTypes.size();
    }

    public Set<FlightEvent.Type> getActiveEvents() {
        return events.clone();
    }

    public void setEvent(FlightEvent.Type type, boolean active) {
        if (active) {
            events.add(type);
        } else {
            events.remove(type);
        }
    }

    public boolean isEventActive(FlightEvent.Type type) {
        return events.contains(type);
    }

    public List<Axis> getAllAxes() {
        List<Axis> list = new ArrayList<Axis>();
        list.addAll(allAxes);
        return list;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * Returns the name of this PlotConfiguration.
     */
    @Override
    public String toString() {
        return name;
    }

    /**
     * Find the best combination of the auto-selectable axes.
     *
     * @return	a new PlotConfiguration with the best fitting auto-selected axes and
     * 			axes ranges selected.
     */
    public PlotConfiguration fillAutoAxes(FlightDataBranch data) {
        PlotConfiguration config = recursiveFillAutoAxes(data).getU();
        config.fitAxes(data);
        return config;
    }

    /**
     * Recursively search for the best combination of the auto-selectable axes.
     * This is a brute-force search method.
     *
     * @return	a new PlotConfiguration with the best fitting auto-selected axes and
     * 			axes ranges selected, and the goodness value
     */
    private Pair<PlotConfiguration, Double> recursiveFillAutoAxes(FlightDataBranch data) {
        // Create copy to fill in
        PlotConfiguration copy = this.clone();
        int autoindex;
        for (autoindex = 0; autoindex < plotDataAxes.size(); autoindex++) {
            if (plotDataAxes.get(autoindex) < 0)
                break;
        }
        if (autoindex >= plotDataAxes.size()) {
            // All axes have been assigned, just return since we are already the best
            return new Pair<PlotConfiguration, Double>(copy, copy.getGoodnessValue(data));
        }
        // Set the auto-selected index one at a time and choose the best one
        PlotConfiguration best = null;
        double bestValue = Double.NEGATIVE_INFINITY;
        for (int i = 0; i < axesCount; i++) {
            copy.plotDataAxes.set(autoindex, i);
            Pair<PlotConfiguration, Double> result = copy.recursiveFillAutoAxes(data);
            if (result.getV() > bestValue) {
                best = result.getU();
                bestValue = result.getV();
            }
        }
        return new Pair<PlotConfiguration, Double>(best, bestValue);
    }

    /**
     * Fit the axes to hold the provided data.  All of the plotDataAxis elements must
     * be non-negative.
     *

     * NOTE: This method assumes that only two axes are used.
     */
    protected void fitAxes(FlightDataBranch data) {
        // Reset axes
        for (Axis a : allAxes) {
            a.reset();
        }
        // Add full range to the axes
        int length = plotDataTypes.size();
        for (int i = 0; i < length; i++) {
            FlightDataType type = plotDataTypes.get(i);
            Unit unit = plotDataUnits.get(i);
            int index = plotDataAxes.get(i);
            if (index < 0) {
                throw new IllegalStateException("fitAxes called with auto-selected axis");
            }
            Axis axis = allAxes.get(index);
            double min = unit.toUnit(data.getMinimum(type));
            double max = unit.toUnit(data.getMaximum(type));
            axis.addBound(min);
            axis.addBound(max);
        }
        // Ensure non-zero (or NaN) range, add a few percent range, include zero if it is close
        for (Axis a : allAxes) {
            if (MathUtil.equals(a.getMinValue(), a.getMaxValue())) {
                a.addBound(a.getMinValue() - 1);
                a.addBound(a.getMaxValue() + 1);
            }
            double addition = a.getRangeLength() * 0.03;
            a.addBound(a.getMinValue() - addition);
            a.addBound(a.getMaxValue() + addition);
            double dist;
            dist = Math.min(Math.abs(a.getMinValue()), Math.abs(a.getMaxValue()));
            if (dist <= a.getRangeLength() * INCLUDE_ZERO_DISTANCE) {
                a.addBound(0);
            }
        }
        // Check whether to use a common zero
        Axis left = allAxes.get(0);
        Axis right = allAxes.get(1);
        if (left.getMinValue() > 0 || left.getMaxValue() < 0 || right.getMinValue() > 0 || right.getMaxValue() < 0 || Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
            return;
        // // Compute common zero
        // TODO: MEDIUM: This algorithm may require tweaking
        double min1 = left.getMinValue();
        double max1 = left.getMaxValue();
        double min2 = right.getMinValue();
        double max2 = right.getMaxValue();
        // Calculate and round scaling factor
        double scale = Math.max(left.getRangeLength(), right.getRangeLength()) / Math.min(left.getRangeLength(), right.getRangeLength());
        scale = roundScale(scale);
        if (right.getRangeLength() > left.getRangeLength()) {
            scale = 1 / scale;
        }
        // Scale right axis, enlarge axes if necessary and scale back
        min2 *= scale;
        max2 *= scale;
        min1 = Math.min(min1, min2);
        min2 = min1;
        max1 = Math.max(max1, max2);
        max2 = max1;
        min2 /= scale;
        max2 /= scale;
        // Apply scale
        left.addBound(min1);
        left.addBound(max1);
        right.addBound(min2);
        right.addBound(max2);
    }

    private double roundScale(double scale) {
        double mul = 1;
        while (scale >= 10) {
            scale /= 10;
            mul *= 10;
        }
        while (scale < 1) {
            scale *= 10;
            mul /= 10;
        }
        if (scale > 7.5) {
            scale = 10;
        } else if (scale > 4.5) {
            scale = 5;
        } else if (scale > 3) {
            scale = 4;
        } else if (scale > 1.5) {
            scale = 2;
        } else {
            scale = 1;
        }
        return scale * mul;
    }

    @SuppressWarnings("unused")
    private double roundScaleUp(double scale) {
        double mul = 1;
        while (scale >= 10) {
            scale /= 10;
            mul *= 10;
        }
        while (scale < 1) {
            scale *= 10;
            mul /= 10;
        }
        if (scale > 5) {
            scale = 10;
        } else if (scale > 4) {
            scale = 5;
        } else if (scale > 2) {
            scale = 4;
        } else if (scale > 1) {
            scale = 2;
        } else {
            scale = 1;
        }
        return scale * mul;
    }

    @SuppressWarnings("unused")
    private double roundScaleDown(double scale) {
        double mul = 1;
        while (scale >= 10) {
            scale /= 10;
            mul *= 10;
        }
        while (scale < 1) {
            scale *= 10;
            mul /= 10;
        }
        if (scale > 5) {
            scale = 5;
        } else if (scale > 4) {
            scale = 4;
        } else if (scale > 2) {
            scale = 2;
        } else {
            scale = 1;
        }
        return scale * mul;
    }

    /**
     * Fits the axis ranges to the data and returns the "goodness value" of this
     * selection of axes.  All plotDataAxis elements must be non-null.
     *

     * NOTE: This method assumes that all data can fit into the axes ranges and
     * that only two axes are used.
     *
     * @return	a "goodness value", the larger the better.
     */
    protected double getGoodnessValue(FlightDataBranch data) {
        double goodness = 0;
        int length = plotDataTypes.size();
        // Fit the axes ranges to the data
        fitAxes(data);
        /*
		 * Calculate goodness of ranges.  100 points is given if the values fill the
		 * entire range, 0 if they fill none of it.
		 */
        for (int i = 0; i < length; i++) {
            FlightDataType type = plotDataTypes.get(i);
            Unit unit = plotDataUnits.get(i);
            int index = plotDataAxes.get(i);
            if (index < 0) {
                throw new IllegalStateException("getGoodnessValue called with auto-selected axis");
            }
            Axis axis = allAxes.get(index);
            double min = unit.toUnit(data.getMinimum(type));
            double max = unit.toUnit(data.getMaximum(type));
            if (Double.isNaN(min) || Double.isNaN(max))
                continue;
            if (MathUtil.equals(min, max))
                continue;
            double d = (max - min) / axis.getRangeLength();
            // Prioritize small ranges
            d = MathUtil.safeSqrt(d);
            goodness += d * 100.0;
        }
        /*
		 * Add extra points for specific things.
		 */
        // A little for the first type being on the first axis
        if (plotDataAxes.get(0) == 0)
            goodness += BONUS_FIRST_TYPE_ON_FIRST_AXIS;
        // A little bonus if the first axis contains zero
        Axis left = allAxes.get(0);
        if (left.getMinValue() <= 0 && left.getMaxValue() >= 0)
            goodness += BONUS_FIRST_AXIS_HAS_ZERO;
        // A boost if a common zero was used in the ranging
        Axis right = allAxes.get(1);
        if (left.getMinValue() <= 0 && left.getMaxValue() >= 0 && right.getMinValue() <= 0 && right.getMaxValue() >= 0)
            goodness += BONUS_COMMON_ZERO;
        // A boost if only one axis is used
        if (Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
            goodness += BONUS_ONLY_ONE_AXIS;
        return goodness;
    }

    /**
     * Reset the units of this configuration to the default units. Returns this
     * PlotConfiguration.
     *
     * @return   this PlotConfiguration.
     */
    public PlotConfiguration resetUnits() {
        for (int i = 0; i < plotDataTypes.size(); i++) {
            plotDataUnits.set(i, plotDataTypes.get(i).getUnitGroup().getDefaultUnit());
        }
        return this;
    }

    @Override
    public PlotConfiguration clone() {
        try {
            PlotConfiguration copy = (PlotConfiguration) super.clone();
            // Shallow-clone all immutable lists
            copy.plotDataTypes = this.plotDataTypes.clone();
            copy.plotDataAxes = this.plotDataAxes.clone();
            copy.plotDataUnits = this.plotDataUnits.clone();
            copy.events = this.events.clone();
            // Deep-clone all Axis since they are mutable
            copy.allAxes = new ArrayList<Axis>();
            for (Axis a : this.allAxes) {
                copy.allAxes.add(a.clone());
            }
            return copy;
        } catch (CloneNotSupportedException e) {
            throw new BugException("BUG! Could not clone().");
        }
    }
}