/* * Copyright 2012 GitHub 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.github.mobile.gauges.ui.airtraffic; import static android.graphics.Bitmap.createScaledBitmap; import static java.lang.Math.PI; import static java.lang.Math.log; import static java.lang.Math.max; import static java.lang.Math.min; import static java.lang.Math.round; import static java.lang.Math.sin; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; import com.github.mobile.gauges.R.color; import com.github.mobile.gauges.R.drawable; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.AnimatorListenerAdapter; import com.nineoldandroids.animation.ObjectAnimator; import com.nineoldandroids.animation.ValueAnimator; import com.nineoldandroids.animation.ValueAnimator.AnimatorUpdateListener; import java.util.Collection; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; /** * View to display an Air Traffic map */ public class AirTrafficView extends View { /** * The maximum numbers of hits to retain */ private static final int MAX_HITS = 500; /** * Size scales the ring goes through while being animated */ private static final float[] RING_SIZES = new float[] { .5F, .625F, .75F, .875F, 1.0F }; /** * Ring Animation */ protected class RingAnimation { private int state; private final Bitmap ring; private final float x; private final float y; /** * Create animation for hit * * @param x * @param y * @param ring */ public RingAnimation(final float x, final float y, final Bitmap ring) { this.ring = ring; this.x = x; this.y = y; } /** * @param state */ public void setState(final int state) { this.state = state; } /** * @return size */ public int getState() { return state; } /** * Draw ring on canvas * * @param canvas * @param ringPaint paint used for drawing rings, won't affect other UI elements */ public void onDraw(final Canvas canvas, final Paint ringPaint) { if (state >= RING_SIZES.length) return; RectF destination = new RectF(); int width = round(ringWidth * RING_SIZES[state]); int height = round(ringHeight * RING_SIZES[state]); destination.top = y - height / 2; destination.left = x - width / 2; destination.right = destination.left + width; destination.bottom = destination.top + height; ringPaint.setAlpha(round(255F - (((float) state / RING_SIZES.length) * 255F))); canvas.drawBitmap(ring, resourceProvider.getRingBounds(), destination, ringPaint); } } /** * Pin image drawn at a hit's location */ protected class PinHit { private final RectF bounds; private final Bitmap pin; /** * Create pin image to draw for hit * * @param x * @param y * @param pin */ public PinHit(final float x, final float y, final Bitmap pin) { bounds = new RectF(); bounds.top = y - pinHeight / 2; bounds.left = x - pinWidth / 2; bounds.right = bounds.left + pinWidth; bounds.bottom = bounds.top + pinHeight; this.pin = pin; } /** * Draw pin on canvas * * @param canvas */ public void onDraw(final Canvas canvas) { canvas.drawBitmap(pin, resourceProvider.getPinBounds(), bounds, mapPaint); } } private static final String MAP_LABEL = "AirTraffic Live"; /** * Divisor used to compute the scaling value * <p> * Constant taken from gaug.es site */ private static final double SCALE_DIVISOR = 720.0; /** * Corrector used to adjust X position */ private static final double X_CORRECTOR = 1.1; /** * Corrector used to adjust Y position */ private static final double Y_CORRECTOR = 70.0; /** * Multiplier used to compute the scaling value * <p> * Constant taken from gaug.es site */ private static final double SCALE_MULTIPLIER = 0.169; /** * Constant taken from gaug.es site */ private static final double PIXELS_PER_LONGITUDE_DEGREE = 16.0 / 360.0; /** * Constant taken from gaug.es site */ private static final double NEGATIVE_PIXELS_PER_LONGITUDE_RADIAN = -(16.0 / (2.0 * PI)); /** * Constant taken from gaug.es site */ private static final double BITMAP_ORIGIN = 16.0 / 2.0; private AirTrafficResourceProvider resourceProvider; /** * Scale value used based on map image dimensions */ private double scale; /** * Correction value used to adjust scaled y position */ private double yCorrector; /** * Correction value used to adjust scaled x position */ private double xCorrector; private int pinHeight; private int pinWidth; private int ringHeight; private int ringWidth; private float mapLabelWidth; private boolean running = true; private final Collection<ObjectAnimator> rings = new ConcurrentLinkedQueue<ObjectAnimator>(); private double xMapScale; private double yMapScale; private Bitmap map; private Bitmap fittedMap; private final Paint mapPaint = new Paint(); private final Paint ringPaint = new Paint(); private final Queue<PinHit> hits = new ConcurrentLinkedQueue<PinHit>(); /** * Constructor. Create objects used throughout the life of the View: the Paint and the animator * * @param context * @param attrs */ public AirTrafficView(Context context, AttributeSet attrs) { super(context, attrs); final Resources resources = getResources(); map = BitmapFactory.decodeResource(resources, drawable.map); mapPaint.setColor(resources.getColor(color.text)); mapPaint.setAntiAlias(true); mapPaint.setSubpixelText(true); mapPaint.setFilterBitmap(true); ringPaint.setAntiAlias(true); ringPaint.setFilterBitmap(true); } /** * Set the height to use when drawing text on the map * * @param height * @return this view */ public AirTrafficView setLabelHeight(final float height) { mapPaint.setTextSize(height); mapLabelWidth = mapPaint.measureText(MAP_LABEL); return this; } /** * Set resource provider * * @param provider * @return this view */ public AirTrafficView setResourceProvider(final AirTrafficResourceProvider provider) { this.resourceProvider = provider; pinHeight = provider.getPinHeight(); pinWidth = provider.getPinWidth(); ringHeight = provider.getRingHeight(); ringWidth = provider.getRingWidth(); return this; } @Override protected void onSizeChanged(final int width, final int height, final int oldw, final int oldh) { super.onSizeChanged(width, height, oldw, oldh); xMapScale = (double) width / map.getWidth(); yMapScale = (double) height / map.getHeight(); double relativeWidth = map.getWidth() / SCALE_DIVISOR; scale = relativeWidth * SCALE_MULTIPLIER; xCorrector = X_CORRECTOR * relativeWidth; yCorrector = Y_CORRECTOR * relativeWidth; fittedMap = createScaledBitmap(map, width, height, true); hits.clear(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (fittedMap != null) canvas.drawBitmap(fittedMap, 0, 0, mapPaint); if (mapLabelWidth > 0) canvas.drawText(MAP_LABEL, fittedMap.getWidth() / 2 - mapLabelWidth / 2, fittedMap.getHeight() - mapPaint.getTextSize(), mapPaint); for (PinHit hit : hits) hit.onDraw(canvas); for (ObjectAnimator ring : rings) ((RingAnimation) ring.getTarget()).onDraw(canvas, ringPaint); } /** * Calculate the x location of the given hit on the map * * @param hit * @return x coordinate */ protected float calculateScreenX(final Hit hit) { // Determine the x positions to draw the hit at. // This code was taken from the gaug.es site double globalX = (BITMAP_ORIGIN + hit.lon * PIXELS_PER_LONGITUDE_DEGREE) * 256.0; float x = (float) ((globalX * scale) - xCorrector); // Take absolute positions on actual map and scale to actual screen size since map image may have been // scaled return (float) (x * xMapScale); } /** * Calculate the y location of the given hit on the map * * @param hit * @return y coordinate */ protected float calculateScreenY(final Hit hit) { // Determine the x and y positions to draw the hit at. // This code was taken from the gaug.es site double e = sin(hit.lat * (PI / 180.0)); e = max(min(e, 0.9999), -0.9999); double globalY = (BITMAP_ORIGIN + 0.5 * log((1.0 + e) / (1.0 - e)) * NEGATIVE_PIXELS_PER_LONGITUDE_RADIAN) * 256.0; float y = (float) ((globalY * scale) - yCorrector); // Take absolute positions on actual map and scale to actual screen size since map image may have been // scaled return (float) (y * yMapScale); } /** * Add hit to view * * @param newHit */ public void addHit(Hit newHit) { if (!running) return; int resourceKey = resourceProvider.getKey(newHit.siteId); if (resourceKey == -1) return; float x = calculateScreenX(newHit); float y = calculateScreenY(newHit); PinHit pinHit = new PinHit(x, y, resourceProvider.getPin(resourceKey)); RingAnimation ringAnimation = new RingAnimation(x, y, resourceProvider.getRing(resourceKey)); hits.add(pinHit); while (hits.size() >= MAX_HITS) hits.poll(); ObjectAnimator animator = ObjectAnimator.ofInt(ringAnimation, "state", 0, RING_SIZES.length); animator.setDuration(500); animator.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { rings.remove(animation); } }); animator.addUpdateListener(new AnimatorUpdateListener() { public void onAnimationUpdate(ValueAnimator animation) { postInvalidate(); } }); animator.start(); rings.add(animator); postInvalidate(); } /** * Pause the animated view */ public void pause() { running = false; for (ObjectAnimator animator : rings) animator.end(); rings.clear(); } /** * Resume the animated view */ public void resume() { running = true; } }