/** * Implementation of the {@link Layer} interface which reads its data from a file during the {@link * Layer#initialize} method. * * @author Brent Bryan * @author John Taylor */ public abstract class AbstractFileBasedLayer extends AbstractSourceLayer { private static final String TAG = MiscUtil.getTag(AbstractFileBasedLayer.class); private static final Executor BACKGROUND_EXECUTOR = Executors.newFixedThreadPool(1); private final AssetManager assetManager; private final String fileName; private final List<AstronomicalSource> fileSources = new ArrayList<>(); public AbstractFileBasedLayer(AssetManager assetManager, Resources resources, String fileName) { super(resources, false); this.assetManager = assetManager; this.fileName = fileName; } @Override public void initialize() { BACKGROUND_EXECUTOR.execute( new Runnable() { public void run() { readSourceFile(fileName); AbstractFileBasedLayer.super.initialize(); } }); } @Override protected void initializeAstroSources(ArrayList<AstronomicalSource> sources) { sources.addAll(fileSources); } private void readSourceFile(String sourceFilename) { Log.d(TAG, "Loading Proto File: " + sourceFilename + "..."); InputStream in = null; try { in = assetManager.open(sourceFilename, AssetManager.ACCESS_BUFFER); AstronomicalSourcesProto.Builder builder = AstronomicalSourcesProto.newBuilder(); builder.mergeFrom(in); for (AstronomicalSourceProto proto : builder.build().getSourceList()) { fileSources.add(new ProtobufAstronomicalSource(proto, getResources())); } Log.d(TAG, "Found: " + fileSources.size() + " sources"); String s = String.format( "Finished Loading: %s | Found %s sourcs.\n", sourceFilename, fileSources.size()); Blog.d(this, s); refreshSources(EnumSet.of(UpdateType.Reset)); } catch (IOException e) { Log.e(TAG, "Unable to open " + sourceFilename); } finally { Closeables.closeQuietly(in); } } }
// TODO(brent): merge with AbstractLayer? public abstract class AbstractSourceLayer extends AbstractLayer { private static final String TAG = MiscUtil.getTag(AbstractSourceLayer.class); private final ArrayList<TextSource> textSources = new ArrayList<TextSource>(); private final ArrayList<ImageSource> imageSources = new ArrayList<ImageSource>(); private final ArrayList<PointSource> pointSources = new ArrayList<PointSource>(); private final ArrayList<LineSource> lineSources = new ArrayList<LineSource>(); private final ArrayList<AstronomicalSource> astroSources = new ArrayList<AstronomicalSource>(); private HashMap<String, SearchResult> searchIndex = new HashMap<String, SearchResult>(); private PrefixStore prefixStore = new PrefixStore(); private final boolean shouldUpdate; private SourceUpdateClosure closure; public AbstractSourceLayer(Resources resources, boolean shouldUpdate) { super(resources); this.shouldUpdate = shouldUpdate; } @Override public synchronized void initialize() { astroSources.clear(); Log.d("RLP", "RLP this is initialization in Abstract source layer"); initializeAstroSources(astroSources); for (AstronomicalSource astroSource : astroSources) { Sources sources = astroSource.initialize(); textSources.addAll(sources.getLabels()); imageSources.addAll(sources.getImages()); pointSources.addAll(sources.getPoints()); lineSources.addAll(sources.getLines()); List<String> names = astroSource.getNames(); if (!names.isEmpty()) { GeocentricCoordinates searchLoc = astroSource.getSearchLocation(); for (String name : names) { searchIndex.put(name.toLowerCase(), new SearchResult(name, searchLoc)); prefixStore.add(name.toLowerCase()); } } } // update the renderer updateLayerForControllerChange(); } @Override protected void updateLayerForControllerChange() { refreshSources(EnumSet.of(UpdateType.Reset)); if (shouldUpdate) { if (closure == null) { closure = new SourceUpdateClosure(this); } addUpdateClosure(closure); } } /** * Subclasses should override this method and add all their {@link AstronomicalSource} to the * given {@link ArrayList}. */ protected abstract void initializeAstroSources(ArrayList<AstronomicalSource> sources); /** * Redraws the sources on this layer, after first refreshing them based on the current state of * the {@link com.google.android.stardroid.control.AstronomerModel}. */ protected void refreshSources() { refreshSources(EnumSet.noneOf(UpdateType.class)); } /** * Redraws the sources on this layer, after first refreshing them based on the current state of * the {@link com.google.android.stardroid.control.AstronomerModel}. */ protected synchronized void refreshSources(EnumSet<UpdateType> updateTypes) { for (AstronomicalSource astroSource : astroSources) { updateTypes.addAll(astroSource.update()); } if (!updateTypes.isEmpty()) { redraw(updateTypes); } } /** Forcefully resets and redraws all sources on this layer everything on this layer. */ @Override protected void redraw() { refreshSources(EnumSet.of(UpdateType.Reset)); } private final void redraw(EnumSet<UpdateType> updateTypes) { super.redraw(textSources, pointSources, lineSources, imageSources, updateTypes); } @Override public List<SearchResult> searchByObjectName(String name) { Log.d(TAG, "Search planets layer for " + name); List<SearchResult> matches = new ArrayList<SearchResult>(); SearchResult searchResult = searchIndex.get(name.toLowerCase()); if (searchResult != null) { matches.add(searchResult); } Log.d(TAG, getLayerName() + " provided " + matches.size() + "results for " + name); return matches; } @Override public Set<String> getObjectNamesMatchingPrefix(String prefix) { Log.d(TAG, "Searching planets layer for prefix " + prefix); Set<String> results = prefixStore.queryByPrefix(prefix); Log.d(TAG, "Got " + results.size() + " results for prefix " + prefix + " in " + getLayerName()); return results; } /** Implementation of the {@link UpdateClosure} interface used to update a layer */ public static class SourceUpdateClosure extends AbstractUpdateClosure { private final AbstractSourceLayer layer; public SourceUpdateClosure(AbstractSourceLayer layer) { this.layer = layer; } @Override public void run() { layer.refreshSources(); } } }
/** * Controls time as selected / created by the user in Time Travel mode. Includes control for * "playing" through time in both directions at different speeds. * * @author Dominic Widdows * @author John Taylor */ public class TimeTravelClock implements Clock { /** A data holder for the time stepping speeds. */ private static class Speed { /** The speed in seconds per second. */ public double rate; /** The id of the Speed's string label. */ public int labelTag; public Speed(double rate, int labelTag) { this.rate = rate; this.labelTag = labelTag; } } public static final long STOPPED = 0; private static final Speed[] SPEEDS = { new Speed(-SECONDS_PER_WEEK, R.string.time_travel_week_speed_back), new Speed(-SECONDS_PER_DAY, R.string.time_travel_day_speed_back), new Speed(-SECONDS_PER_HOUR, R.string.time_travel_hour_speed_back), new Speed(-SECONDS_PER_10MINUTE, R.string.time_travel_10minute_speed_back), new Speed(-SECONDS_PER_MINUTE, R.string.time_travel_minute_speed_back), new Speed(-SECONDS_PER_SECOND, R.string.time_travel_second_speed_back), new Speed(STOPPED, R.string.time_travel_stopped), new Speed(SECONDS_PER_SECOND, R.string.time_travel_second_speed), new Speed(SECONDS_PER_MINUTE, R.string.time_travel_minute_speed), new Speed(SECONDS_PER_10MINUTE, R.string.time_travel_10minute_speed), new Speed(SECONDS_PER_HOUR, R.string.time_travel_hour_speed), new Speed(SECONDS_PER_DAY, R.string.time_travel_day_speed), new Speed(SECONDS_PER_WEEK, R.string.time_travel_week_speed), }; private static final int STOPPED_INDEX = SPEEDS.length / 2; private int speedIndex = STOPPED_INDEX; private static final String TAG = MiscUtil.getTag(TimeTravelClock.class); private long timeLastSet; private long simulatedTime; /** * Sets the internal time. * * @param date Date to which the timeTravelDate will be set. */ public synchronized void setTimeTravelDate(Date date) { pauseTime(); timeLastSet = System.currentTimeMillis(); simulatedTime = date.getTime(); } /* * Controller logic for playing through time at different directions and * speeds. */ /** * Increases the rate of time travel into the future (or decreases the rate of time travel into * the past.) */ public synchronized void accelerateTimeTravel() { if (speedIndex < SPEEDS.length - 1) { Log.d(TAG, "Accelerating speed to: " + SPEEDS[speedIndex]); ++speedIndex; } else { Log.d(TAG, "Already at max forward speed"); } } /** * Decreases the rate of time travel into the future (or increases the rate of time travel into * the past.) */ public synchronized void decelerateTimeTravel() { if (speedIndex > 0) { Log.d(TAG, "Decelerating speed to: " + SPEEDS[speedIndex]); --speedIndex; } else { Log.d(TAG, "Already at maximum backwards speed"); } } /** Pauses time. */ public synchronized void pauseTime() { Log.d(TAG, "Pausing time"); assert SPEEDS[STOPPED_INDEX].rate == 0.0; speedIndex = STOPPED_INDEX; } /** @return The current speed tag, a string describing the speed of time travel. */ public int getCurrentSpeedTag() { return SPEEDS[speedIndex].labelTag; } @Override public long getTimeInMillisSinceEpoch() { long now = System.currentTimeMillis(); long elapsedTimeMillis = now - timeLastSet; double rate = SPEEDS[speedIndex].rate; long timeDelta = (long) (rate * elapsedTimeMillis); if (Math.abs(rate) >= SECONDS_PER_DAY) { // For speeds greater than or equal to 1 day/sec we want to move in // increments of 1 day so that the map isn't dizzyingly fast. // This shows the slow annual procession of the stars. long days = (long) (timeDelta / MILLISECONDS_PER_DAY); if (days == 0) { return simulatedTime; } // Note that this assumes that time requests will occur right on the // day boundary. If they occur later then the next time jump // might be a bit shorter than it should be. Nevertheless the refresh // rate of the renderer is high enough that this should be unnoticeable. timeDelta = (long) (days * MILLISECONDS_PER_DAY); } timeLastSet = now; simulatedTime += timeDelta; return simulatedTime; } }
/** * Base implementation of the {@link Layer} interface. * * @author John Taylor * @author Brent Bryan */ public abstract class AbstractLayer implements Layer { private static final String TAG = MiscUtil.getTag(AbstractLayer.class); private final ReentrantLock renderMapLock = new ReentrantLock(); private final HashMap<Class<?>, RenderManager<?>> renderMap = Maps.newHashMap(); private final Resources resources; private RendererController renderer; public AbstractLayer(Resources resources) { this.resources = resources; } protected Resources getResources() { return resources; } @Override public void registerWithRenderer(RendererController rendererController) { this.renderMap.clear(); this.renderer = rendererController; updateLayerForControllerChange(); } protected abstract void updateLayerForControllerChange(); @Override public void setVisible(boolean visible) { Log.w(TAG, "RLP setVisible: entry"); renderMapLock.lock(); try { if (renderer == null) { Log.w( TAG, "RLP setVisible: Renderer not set - aborting " + this.getClass().getSimpleName()); return; } AtomicSection atomic = renderer.createAtomic(); for (Entry<Class<?>, RenderManager<?>> entry : renderMap.entrySet()) { entry.getValue().queueEnabled(visible, atomic); } renderer.queueAtomic(atomic); } finally { renderMapLock.unlock(); } } protected void addUpdateClosure(UpdateClosure closure) { if (renderer != null) { renderer.addUpdateClosure(closure); } } protected void removeUpdateClosure(UpdateClosure closure) { if (renderer != null) { renderer.removeUpdateCallback(closure); } } /** * Forces a redraw of this layer, clearing all of the information about this layer in the renderer * and repopulating it. */ protected abstract void redraw(); protected void redraw( final ArrayList<TextSource> textSources, final ArrayList<PointSource> pointSources, final ArrayList<LineSource> lineSources, final ArrayList<ImageSource> imageSources) { redraw(textSources, pointSources, lineSources, imageSources, EnumSet.of(UpdateType.Reset)); } /** * Updates the renderer (using the given {@link UpdateType}), with then given set of UI elements. * Depending on the value of {@link UpdateType}, current sources will either have their state * updated, or will be overwritten by the given set of UI elements. */ protected void redraw( final ArrayList<TextSource> textSources, final ArrayList<PointSource> pointSources, final ArrayList<LineSource> lineSources, final ArrayList<ImageSource> imageSources, EnumSet<UpdateType> updateTypes) { // Log.d(TAG, getLayerName() + "RLP Updating renderer: " + updateTypes); if (renderer == null) { Log.w(TAG, "RLP Renderer not set - aborting: " + this.getClass().getSimpleName()); return; } renderMapLock.lock(); try { // Blog.d(this, "Redraw: " + updateTypes); AtomicSection atomic = renderer.createAtomic(); setSources(textSources, updateTypes, TextSource.class, atomic); setSources(pointSources, updateTypes, PointSource.class, atomic); setSources(lineSources, updateTypes, LineSource.class, atomic); setSources(imageSources, updateTypes, ImageSource.class, atomic); renderer.queueAtomic(atomic); } finally { renderMapLock.unlock(); } } /** * Sets the objects on the {@link RenderManager} to the given values, creating (or disabling) the * {@link RenderManager} if necessary. */ private <E> void setSources( ArrayList<E> sources, EnumSet<UpdateType> updateType, Class<E> clazz, AtomicSection atomic) { @SuppressWarnings("unchecked") RenderManager<E> manager = (RenderManager<E>) renderMap.get(clazz); if (sources == null || sources.isEmpty()) { if (manager != null) { // TODO(brent): we should really just disable this layer, but in a // manner that it will automatically be reenabled when appropriate. Blog.d(this, " " + clazz.getSimpleName()); manager.queueObjects(Collections.<E>emptyList(), updateType, atomic); } return; } if (manager == null) { manager = createRenderManager(clazz, atomic); renderMap.put(clazz, manager); } // Blog.d(this, " " + clazz.getSimpleName() + " " + sources.size()); manager.queueObjects(sources, updateType, atomic); } @SuppressWarnings("unchecked") <E> RenderManager<E> createRenderManager(Class<E> clazz, RendererControllerBase controller) { if (clazz.equals(ImageSource.class)) { return (RenderManager<E>) controller.createImageManager(getLayerId()); } else if (clazz.equals(TextSource.class)) { return (RenderManager<E>) controller.createLabelManager(getLayerId()); } else if (clazz.equals(LineSource.class)) { return (RenderManager<E>) controller.createLineManager(getLayerId()); } else if (clazz.equals(PointSource.class)) { return (RenderManager<E>) controller.createPointManager(getLayerId()); } throw new IllegalStateException("Unknown source type: " + clazz); } @Override public List<SearchResult> searchByObjectName(String name) { // By default, layers will return no search results. // Override this if the layer should be searchable. return Collections.emptyList(); } @Override public Set<String> getObjectNamesMatchingPrefix(String prefix) { // By default, layers will return no search results. // Override this if the layer should be searchable. return Collections.emptySet(); } /** Provides a string ID to the internationalized name of this layer. */ // TODO(brent): could this be combined with getLayerId? Not sure - they // serve slightly different purposes. protected abstract int getLayerNameId(); @Override public String getPreferenceId() { return getPreferenceId(getLayerNameId()); } protected String getPreferenceId(int layerNameId) { return "source_provider." + layerNameId; } @Override public String getLayerName() { return getStringFromId(getLayerNameId()); } /** Return an internationalized string from a string resource id. */ protected String getStringFromId(int resourceId) { return resources.getString(resourceId); } }
/** * If enabled, keeps the sky gradient up to date. * * @author John Taylor * @author Brent Bryan */ public class SkyGradientLayer implements Layer { private static final String TAG = MiscUtil.getTag(SkyGradientLayer.class); private static final long UPDATE_FREQUENCY_MS = 5L * TimeConstants.MILLISECONDS_PER_MINUTE; private final ReentrantLock rendererLock = new ReentrantLock(); private final AstronomerModel model; private final Resources resources; private RendererController renderer; private long lastUpdateTimeMs = 0L; public SkyGradientLayer(AstronomerModel model, Resources resources) { this.model = model; this.resources = resources; } @Override public void initialize() {} @Override public void registerWithRenderer(RendererController controller) { this.renderer = controller; redraw(); } @Override public void setVisible(boolean visible) { Log.d(TAG, "Setting showSkyGradient " + visible); if (visible) { redraw(); } else { rendererLock.lock(); try { renderer.queueDisableSkyGradient(); } finally { rendererLock.unlock(); } } } /** Redraws the sky shading gradient using the model's current time. */ protected void redraw() { Date modelTime = model.getTime(); if (Math.abs(modelTime.getTime() - lastUpdateTimeMs) > UPDATE_FREQUENCY_MS) { lastUpdateTimeMs = modelTime.getTime(); RaDec sunPosition = SolarPositionCalculator.getSolarPosition(modelTime); // Log.d(TAG, "Enabling sky gradient with sun position " + sunPosition); rendererLock.lock(); try { renderer.queueEnableSkyGradient(GeocentricCoordinates.getInstance(sunPosition)); } finally { rendererLock.unlock(); } } } @Override public int getLayerId() { return 0; } public String getPreferenceId() { return "source_provider." + getLayerNameId(); } public String getLayerName() { return resources.getString(getLayerNameId()); } private int getLayerNameId() { return R.string.show_sky_gradient; } @Override public List<SearchResult> searchByObjectName(String name) { return Collections.emptyList(); } @Override public Set<String> getObjectNamesMatchingPrefix(String prefix) { return Collections.emptySet(); } }
/** Edit the user's preferences. */ public class EditSettingsActivity extends PreferenceActivity implements OnPreferenceChangeListener { /** These must match the keys in the preference_screen.xml file. */ private static final String LONGITUDE = "longitude"; private static final String LATITUDE = "latitude"; private static final String LOCATION = "location"; private static final String TAG = MiscUtil.getTag(EditSettingsActivity.class); private Geocoder geocoder; private ActivityLightLevelManager activityLightLevelManager; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); activityLightLevelManager = new ActivityLightLevelManager( new ActivityLightLevelChanger(this, null), PreferenceManager.getDefaultSharedPreferences(this)); geocoder = new Geocoder(this); addPreferencesFromResource(R.xml.preference_screen); if (!StardroidApplication.getSupportsNewSensors()) { findPreference("fused_sensor").setEnabled(false); } findPreference("sensor_damping") .setEnabled( !PreferenceManager.getDefaultSharedPreferences(this).getBoolean("fused_sensor", false)); findPreference("fused_sensor").setOnPreferenceChangeListener(this); Preference editPreference = findPreference(LOCATION); // TODO(johntaylor) if the lat long prefs get changed manually, we should really // reset the placename to "" too. editPreference.setOnPreferenceChangeListener( new OnPreferenceChangeListener() { public boolean onPreferenceChange(Preference preference, Object newValue) { Log.d(TAG, "Place to be updated to " + newValue); boolean success = setLatLongFromPlace(newValue.toString()); return success; } }); } public boolean onPreferenceChange(Preference preference, Object newValue) { if (preference.getKey().equals("fused_sensor")) { findPreference("sensor_damping").setEnabled(!newValue.toString().equals("true")); } return true; } @Override public void onStart() { super.onStart(); Analytics.getInstance(this).trackPageView(Analytics.EDIT_SETTINGS_ACTIVITY); } @Override public void onResume() { super.onResume(); activityLightLevelManager.onResume(); } @Override public void onPause() { super.onPause(); updatePreferences(); activityLightLevelManager.onPause(); } /** * Updates preferences on singletons, so we don't have to register preference change listeners for * them. */ private void updatePreferences() { Log.d(TAG, "Updating preferences"); Analytics.getInstance(this).setEnabled(findPreference(Analytics.PREF_KEY).isEnabled()); } protected boolean setLatLongFromPlace(String place) { List<Address> addresses = new ArrayList<Address>(); try { addresses = geocoder.getFromLocationName(place, 1); } catch (IOException e) { Toast.makeText(this, getString(R.string.location_unable_to_geocode), Toast.LENGTH_SHORT) .show(); return false; } if (addresses.size() == 0) { showNotFoundDialog(place); return false; } // TODO(johntaylor) let the user choose, but for now just pick the first. Address first = addresses.get(0); setLatLong(first.getLatitude(), first.getLongitude()); return true; } private void setLatLong(double latitude, double longitude) { EditTextPreference latPreference = (EditTextPreference) findPreference(LATITUDE); EditTextPreference longPreference = (EditTextPreference) findPreference(LONGITUDE); latPreference.setText(Double.toString(latitude)); longPreference.setText(Double.toString(longitude)); String message = String.format(getString(R.string.location_place_found), latitude, longitude); Log.d(TAG, message); Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } private void showNotFoundDialog(String place) { String message = String.format(getString(R.string.location_not_found), place); AlertDialog.Builder dialog = new AlertDialog.Builder(this) .setTitle(R.string.location_not_found_title) .setMessage(message) .setPositiveButton( android.R.string.ok, new OnClickListener() { public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); dialog.show(); } }