// Copyright 2011 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.android.stardroid.layers; import com.google.android.stardroid.R; import com.google.android.stardroid.base.Lists; import com.google.android.stardroid.base.TimeConstants; import com.google.android.stardroid.control.AstronomerModel; import com.google.android.stardroid.renderer.RendererObjectManager.UpdateType; import com.google.android.stardroid.source.AbstractAstronomicalSource; import com.google.android.stardroid.source.AstronomicalSource; import com.google.android.stardroid.source.ImageSource; import com.google.android.stardroid.source.Sources; import com.google.android.stardroid.source.TextSource; import com.google.android.stardroid.source.impl.ImageSourceImpl; import com.google.android.stardroid.source.impl.TextSourceImpl; import com.google.android.stardroid.units.GeocentricCoordinates; import com.google.android.stardroid.units.Vector3; import android.content.res.Resources; import android.text.format.DateFormat; import java.util.ArrayList; import java.util.Date; import java.util.EnumSet; import java.util.List; /** * A {@link Layer} to show well-known meteor showers. * * @author John Taylor */ public class MeteorShowerLayer extends AbstractSourceLayer { private List<Shower> showers = Lists.newArrayList(); /** * Represents a meteor shower. */ private static class Shower { private Date start; private GeocentricCoordinates radiant; private int nameId; private Date peak; private Date end; private int peakMeteorsPerHour; public Shower(String name, int nameId, GeocentricCoordinates radiant, Date start, Date peak, Date end, int peakMeteorsPerHour) { this.nameId = nameId; this.radiant = radiant; this.start = start; this.peak = peak; this.end = end; this.peakMeteorsPerHour = peakMeteorsPerHour; } } private final AstronomerModel model; private static final int ANY_OLD_YEAR = 100; // = year 2000 /** Number of meteors per hour for the larger graphic */ private static final double METEOR_THRESHOLD_PER_HR = 10; public MeteorShowerLayer(AstronomerModel model, Resources resources) { super(resources, true); this.model = model; initializeShowers(); } private void initializeShowers() { // A list of all the meteor showers with > 10 per hour // Source: http://www.imo.net/calendar/2011#table5 // Note the zero-based month. 10=November // Actual start for Quadrantids is December 28 - but we can't cross a year boundary. showers.add(new Shower("quadrantids", R.string.quadrantids, GeocentricCoordinates.getInstance(230, 49), new Date(ANY_OLD_YEAR, 0, 1), new Date(ANY_OLD_YEAR, 0, 4), new Date(ANY_OLD_YEAR, 0, 12), 120)); showers.add(new Shower("lyrids", R.string.lyrids, GeocentricCoordinates.getInstance(271, 34), new Date(ANY_OLD_YEAR, 3, 16), new Date(ANY_OLD_YEAR, 3, 22), new Date(ANY_OLD_YEAR, 3, 25), 18)); showers.add(new Shower("e-aquariids", R.string.aquariids, GeocentricCoordinates.getInstance(338, -1), new Date(ANY_OLD_YEAR, 3, 19), new Date(ANY_OLD_YEAR, 4, 6), new Date(ANY_OLD_YEAR, 4, 28), 70)); showers.add(new Shower("d-aquariids", R.string.deltaaquariids, GeocentricCoordinates.getInstance(340, -16), new Date(ANY_OLD_YEAR, 6, 12), new Date(ANY_OLD_YEAR, 6, 30), new Date(ANY_OLD_YEAR, 7, 23), 16)); showers.add(new Shower("perseids", R.string.perseids, GeocentricCoordinates.getInstance(48, 58), new Date(ANY_OLD_YEAR, 6, 17), new Date(ANY_OLD_YEAR, 7, 13), new Date(ANY_OLD_YEAR, 7, 24), 100)); showers.add(new Shower("orionids", R.string.orionids, GeocentricCoordinates.getInstance(95, 16), new Date(ANY_OLD_YEAR, 9, 2), new Date(ANY_OLD_YEAR, 9, 21), new Date(ANY_OLD_YEAR, 10, 7), 25)); showers.add(new Shower("leonids", R.string.leonids, GeocentricCoordinates.getInstance(152, 22), new Date(ANY_OLD_YEAR, 10, 6), new Date(ANY_OLD_YEAR, 10, 18), new Date(ANY_OLD_YEAR, 10, 30), 20)); showers.add(new Shower("puppid-velids", R.string.puppidvelids, GeocentricCoordinates.getInstance(123, -45), new Date(ANY_OLD_YEAR, 11, 1), new Date(ANY_OLD_YEAR, 11, 7), new Date(ANY_OLD_YEAR, 11, 15), 10)); showers.add(new Shower("geminids", R.string.geminids, GeocentricCoordinates.getInstance(112, 33), new Date(ANY_OLD_YEAR, 11, 7), new Date(ANY_OLD_YEAR, 11, 14), new Date(ANY_OLD_YEAR, 11, 17), 120)); showers.add(new Shower("ursides", R.string.ursids, GeocentricCoordinates.getInstance(217, 76), new Date(ANY_OLD_YEAR, 11, 17), new Date(ANY_OLD_YEAR, 11, 23), new Date(ANY_OLD_YEAR, 11, 26), 10)); } @Override protected void initializeAstroSources(ArrayList<AstronomicalSource> sources) { for (Shower shower : showers) { sources.add(new MeteorRadiantSource(model, shower, getResources())); } } @Override public int getLayerId() { return -106; } @Override public String getPreferenceId() { return "source_provider.6"; } @Override public String getLayerName() { return "Meteor Showers"; } @Override protected int getLayerNameId() { return R.string.show_meteors_pref; } private static class MeteorRadiantSource extends AbstractAstronomicalSource { private static final int LABEL_COLOR = 0xf67e81; private static final Vector3 UP = new Vector3(0.0f, 1.0f, 0.0f); private static final long UPDATE_FREQ_MS = 1L * TimeConstants.MILLISECONDS_PER_DAY; private static final float SCALE_FACTOR = 0.03f; private final List<ImageSource> imageSources = Lists.newArrayList(); private final List<TextSource> labelSources = Lists.newArrayList(); private final AstronomerModel model; private long lastUpdateTimeMs = 0L; private ImageSourceImpl theImage; private TextSource label; private Shower shower; private String name; private List<String> searchNames = Lists.newArrayList(); public MeteorRadiantSource(AstronomerModel model, Shower shower, Resources resources) { this.model = model; this.shower = shower; this.name = resources.getString(shower.nameId); // Not sure what the right user experience should be here. Should we only show up // in the search results when the shower is visible? For now, just ensure // that it's obvious from the search label. CharSequence startDate = DateFormat.format("MMM dd", shower.start); CharSequence endDate = DateFormat.format("MMM dd", shower.end); searchNames.add(name + " (" + startDate + "-" + endDate + ")"); // blank is a 1pxX1px image that should be invisible. // We'd prefer not to show any image except on the shower dates, but there // appears to be a bug in the renderer/layer interface in that Update values are not // respected. Ditto the label. // TODO(johntaylor): fix the bug and remove this blank image theImage = new ImageSourceImpl(shower.radiant, resources, R.drawable.blank, UP, SCALE_FACTOR); imageSources.add(theImage); label = new TextSourceImpl(shower.radiant, name, LABEL_COLOR); labelSources.add(label); } @Override public List<String> getNames() { return searchNames; } @Override public GeocentricCoordinates getSearchLocation() { return shower.radiant; } private void updateShower() { lastUpdateTimeMs = model.getTime().getTime(); // We will only show the shower if it's the right time of year. Date now = model.getTime(); // Standardize on the same year as we stored for the showers. now.setYear(ANY_OLD_YEAR); theImage.setUpVector(UP); // TODO(johntaylor): consider varying the sizes by scaling factor as time progresses. if (now.after(shower.start) && now.before(shower.end)) { label.setText(name); double percentToPeak; if (now.before(shower.peak)) { percentToPeak = (double) (now.getTime() - shower.start.getTime()) / (shower.peak.getTime() - shower.start.getTime()); } else { percentToPeak = (double) (shower.end.getTime() - now.getTime()) / (shower.end.getTime() - shower.peak.getTime()); } // Not sure how best to calculate number of meteors - use linear interpolation for now. double numberOfMeteorsPerHour = shower.peakMeteorsPerHour * percentToPeak; if (numberOfMeteorsPerHour > METEOR_THRESHOLD_PER_HR) { theImage.setImageId(R.drawable.meteor2_screen); } else { theImage.setImageId(R.drawable.meteor1_screen); } } else { label.setText(" "); theImage.setImageId(R.drawable.blank); } } @Override public Sources initialize() { updateShower(); return this; } @Override public EnumSet<UpdateType> update() { EnumSet<UpdateType> updateTypes = EnumSet.noneOf(UpdateType.class); if (Math.abs(model.getTime().getTime() - lastUpdateTimeMs) > UPDATE_FREQ_MS) { updateShower(); updateTypes.add(UpdateType.Reset); } return updateTypes; } @Override public List<? extends ImageSource> getImages() { return imageSources; } @Override public List<? extends TextSource> getLabels() { return labelSources; } } }