package com.nexenio.bleindoorpositioningdemo.ui.beaconview.map; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PointF; import android.graphics.RadialGradient; import android.graphics.RectF; import android.graphics.Shader; import androidx.annotation.CallSuper; import androidx.annotation.Nullable; import android.util.AttributeSet; import com.nexenio.bleindoorpositioning.ble.advertising.AdvertisingPacket; import com.nexenio.bleindoorpositioning.ble.beacon.Beacon; import com.nexenio.bleindoorpositioning.location.Location; import com.nexenio.bleindoorpositioning.location.LocationListener; import com.nexenio.bleindoorpositioning.location.distance.DistanceUtil; import com.nexenio.bleindoorpositioning.location.projection.CanvasProjection; import com.nexenio.bleindoorpositioning.location.projection.EquirectangularProjection; import com.nexenio.bleindoorpositioning.location.provider.LocationProvider; import com.nexenio.bleindoorpositioningdemo.ui.LocationAnimator; import com.nexenio.bleindoorpositioningdemo.ui.beaconview.BeaconView; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; /** * Created by steppschuh on 16.11.17. */ public class BeaconMap extends BeaconView { protected ValueAnimator deviceRadiusAnimator; protected Location topLeftLocation; protected Location bottomRightLocation; protected LocationAnimator topLeftLocationAnimator; protected LocationAnimator bottomRightLocationAnimator; protected CanvasProjection canvasProjection; protected Location predictedDeviceLocation; protected LocationAnimator predictedDeviceLocationAnimator; protected List<Location> recentLocations = new ArrayList<>(); protected BeaconMapBackground mapBackground; protected Matrix backgroundMatrix; protected float matrixScaleFactor; protected PointF matrixTranslationPoint; protected float matrixRotationDegrees; protected Paint historyFillPaint; public BeaconMap(Context context) { super(context); } public BeaconMap(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public BeaconMap(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public BeaconMap(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @CallSuper @Override public void initialize() { super.initialize(); canvasProjection = new CanvasProjection(); historyFillPaint = new Paint(secondaryFillPaint); } @CallSuper @Override protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { super.onSizeChanged(width, height, oldWidth, oldHeight); canvasProjection.setCanvasWidth(canvasWidth); canvasProjection.setCanvasHeight(canvasHeight); } @CallSuper @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawLegend(canvas); } @CallSuper @Override protected void drawBackground(Canvas canvas) { super.drawBackground(canvas); if (mapBackground == null) { return; } matrixScaleFactor = (float) (mapBackground.getMetersPerPixel() / canvasProjection.getMetersPerCanvasUnit()); matrixTranslationPoint = getPointFromLocation(mapBackground.getTopLeftLocation()); matrixRotationDegrees = (float) mapBackground.getBearing(); backgroundMatrix = new Matrix(); backgroundMatrix.postScale(matrixScaleFactor, matrixScaleFactor); backgroundMatrix.postTranslate(matrixTranslationPoint.x, matrixTranslationPoint.y); backgroundMatrix.postRotate(matrixRotationDegrees); canvas.drawBitmap(mapBackground.getImageBitmap(), backgroundMatrix, null); } @Override protected void drawDevice(Canvas canvas) { drawDeviceHistory(canvas); drawDevicePrediction(canvas); if (deviceLocationAnimator == null) { return; } PointF deviceCenter = getPointFromLocation(deviceLocationAnimator.getLocation()); float locationAccuracy = (float) deviceLocationAnimator.getLocation().getAccuracy(); float deviceAccuracyRadius = (float) canvasProjection.getCanvasUnitsFromMeters(locationAccuracy); float animationValue = (deviceRadiusAnimator == null) ? 0 : (float) deviceRadiusAnimator.getAnimatedValue(); float strokeRadius = (pixelsPerDip * 10) + (pixelsPerDip * 2 * animationValue); canvas.drawCircle(deviceCenter.x, deviceCenter.y, deviceAccuracyRadius, deviceRangePaint); canvas.drawCircle(deviceCenter.x, deviceCenter.y, strokeRadius, whiteFillPaint); canvas.drawCircle(deviceCenter.x, deviceCenter.y, strokeRadius, secondaryStrokePaint); canvas.drawCircle(deviceCenter.x, deviceCenter.y, pixelsPerDip * 8, secondaryFillPaint); } protected void drawDeviceHistory(Canvas canvas) { PointF deviceCenter; float heatmapRadius = (float) canvasProjection.getCanvasUnitsFromMeters(1); float recencyScore; int alpha; for (Location location : recentLocations) { if (location == null || !location.hasLatitudeAndLongitude()) { continue; } deviceCenter = getPointFromLocation(location); recencyScore = getRecencyScore(location.getTimestamp(), TimeUnit.SECONDS.toMillis(10)); alpha = (int) (255 * 0.25 * recencyScore); historyFillPaint.setAlpha(alpha); RadialGradient gradient = new RadialGradient(deviceCenter.x, deviceCenter.y, heatmapRadius, new int[]{secondaryFillPaint.getColor(), Color.TRANSPARENT}, null, Shader.TileMode.CLAMP); historyFillPaint.setShader(gradient); canvas.drawCircle(deviceCenter.x, deviceCenter.y, heatmapRadius, historyFillPaint); } } protected void drawDevicePrediction(Canvas canvas) { if (deviceLocationAnimator == null || predictedDeviceLocationAnimator == null) { return; } PointF predictionCenter = getPointFromLocation(predictedDeviceLocationAnimator.getLocation()); canvas.drawLine(getPointFromLocation(deviceLocationAnimator.getLocation()).x, getPointFromLocation(deviceLocationAnimator.getLocation()).y, predictionCenter.x, predictionCenter.y, primaryStrokePaint); } @Override protected void drawBeacons(Canvas canvas) { Map<Beacon, PointF> beaconCenterMap = new HashMap<>(); // draw all backgrounds for (Beacon beacon : beacons) { PointF beaconCenter = getPointFromLocation(beacon.getLocation()); beaconCenterMap.put(beacon, beaconCenter); drawBeaconBackground(canvas, beacon, beaconCenter); } // draw all foregrounds for (Beacon beacon : beacons) { drawBeaconForeground(canvas, beacon, beaconCenterMap.get(beacon)); } } /** * This shouldn't be called, because the created beacon background may overlay existing beacon * foregrounds. Use {@link #drawBeacons(Canvas)} instead. */ @Override protected void drawBeacon(Canvas canvas, Beacon beacon) { PointF beaconCenter = getPointFromLocation(beacon.getLocation()); drawBeaconBackground(canvas, beacon, beaconCenter); drawBeaconForeground(canvas, beacon, beaconCenter); } protected void drawBeaconBackground(Canvas canvas, Beacon beacon, PointF beaconCenter) { float distance = (float) canvasProjection.getCanvasUnitsFromMeters(beacon.getDistance()); canvas.drawCircle(beaconCenter.x, beaconCenter.y, distance, beaconRangePaint); float advertisingRadius = (float) canvasProjection.getCanvasUnitsFromMeters(beacon.getEstimatedAdvertisingRange()); Paint innerBeaconRangePaint = new Paint(beaconRangePaint); innerBeaconRangePaint.setAlpha(100); Shader rangeShader = new RadialGradient( beaconCenter.x, beaconCenter.y, advertisingRadius - (pixelsPerDip * 0), primaryFillPaint.getColor(), beaconRangePaint.getColor(), Shader.TileMode.MIRROR); innerBeaconRangePaint.setShader(rangeShader); //canvas.drawCircle(beaconCenter.x, beaconCenter.y, advertisingRadius, innerBeaconRangePaint); canvas.drawCircle(beaconCenter.x, beaconCenter.y, advertisingRadius, beaconRangePaint); } protected void drawBeaconForeground(Canvas canvas, Beacon beacon, PointF beaconCenter) { AdvertisingPacket latestAdvertisingPacket = beacon.getLatestAdvertisingPacket(); long timeSinceLastAdvertisement = latestAdvertisingPacket != null ? System.currentTimeMillis() - latestAdvertisingPacket.getTimestamp() : 0; float animationValue = (deviceRadiusAnimator == null) ? 0 : (float) deviceRadiusAnimator.getAnimatedValue(); animationValue *= Math.max(0, 1 - (timeSinceLastAdvertisement / 1000)); float beaconRadius = pixelsPerDip * 8; float strokeRadius = beaconRadius + (pixelsPerDip * 2) + (pixelsPerDip * 2 * animationValue); int beaconCornerRadius = (int) pixelsPerDip * 2; RectF rect = new RectF(beaconCenter.x - strokeRadius, beaconCenter.y - strokeRadius, beaconCenter.x + strokeRadius, beaconCenter.y + strokeRadius); canvas.drawRoundRect(rect, beaconCornerRadius, beaconCornerRadius, whiteFillPaint); canvas.drawRoundRect(rect, beaconCornerRadius, beaconCornerRadius, primaryStrokePaint); rect = new RectF(beaconCenter.x - beaconRadius, beaconCenter.y - beaconRadius, beaconCenter.x + beaconRadius, beaconCenter.y + beaconRadius); canvas.drawRoundRect(rect, beaconCornerRadius, beaconCornerRadius, primaryFillPaint); } protected void drawLegend(Canvas canvas) { drawReferenceLine(canvas); } protected void drawReferenceLine(Canvas canvas) { float canvasPadding = canvasWidth * canvasProjection.getPaddingFactor(); float maximumReferenceLineWidth = canvasWidth - (2 * canvasPadding); float maximumReferenceDistance = (float) canvasProjection.getMetersFromCanvasUnits(maximumReferenceLineWidth); float referenceDistance = DistanceUtil.getReasonableSmallerEvenDistance(maximumReferenceDistance); float referenceLineWidth = (float) canvasProjection.getCanvasUnitsFromMeters(referenceDistance); float referenceLinePadding = (canvasWidth - referenceLineWidth) / 2; Paint legendPaint = new Paint(textPaint); legendPaint.setAlpha(50); legendPaint.setTextSize(pixelsPerDip * 12); float referenceYOffset = canvasHeight - (pixelsPerDip * 16); PointF referenceStartPoint = new PointF(referenceLinePadding, referenceYOffset); PointF referenceEndPoint = new PointF(canvasWidth - referenceLinePadding, referenceYOffset); // horizontal line canvas.drawRect( referenceStartPoint.x, referenceStartPoint.y, referenceEndPoint.x, referenceEndPoint.y - pixelsPerDip, legendPaint ); // left vertical line canvas.drawRect( referenceStartPoint.x, referenceStartPoint.y - (pixelsPerDip * 8), referenceStartPoint.x + pixelsPerDip, referenceStartPoint.y, legendPaint ); // right vertical line canvas.drawRect( referenceEndPoint.x, referenceEndPoint.y - (pixelsPerDip * 8), referenceEndPoint.x + pixelsPerDip, referenceEndPoint.y, legendPaint ); // text String referenceText = String.valueOf(Math.round(referenceDistance)) + " meters"; float referenceTextWidth = legendPaint.measureText(referenceText); canvas.drawText( referenceText, (canvasWidth / 2) - (referenceTextWidth / 2), referenceStartPoint.y - (pixelsPerDip * 4), legendPaint ); } protected PointF getPointFromLocation(Location location) { if (location == null || !location.hasLatitudeAndLongitude()) { return new PointF(0, 0); } float x = canvasProjection.getXFromLocation(location); float y = canvasProjection.getYFromLocation(location); return new PointF(x, y); } public void fitToCurrentLocations() { topLeftLocation = null; bottomRightLocation = null; onLocationsChanged(); } private void updateEdgeLocations() { List<Location> locations = new ArrayList<>(); for (Beacon beacon : beacons) { locations.add(beacon.getLocation()); } if (deviceLocationAnimator != null) { locations.add(deviceLocationAnimator.getLocation()); Location deviceTopLeftLocation = new Location(deviceLocationAnimator.getLocation()); deviceTopLeftLocation.setLatitude(deviceTopLeftLocation.getLatitude() + 0.00001); deviceTopLeftLocation.setLongitude(deviceTopLeftLocation.getLongitude() - 0.00002); locations.add(deviceTopLeftLocation); Location deviceBottomRightLocation = new Location(deviceLocationAnimator.getLocation()); deviceBottomRightLocation.setLatitude(deviceBottomRightLocation.getLatitude() - 0.00001); deviceBottomRightLocation.setLongitude(deviceBottomRightLocation.getLongitude() + 0.00002); locations.add(deviceBottomRightLocation); } if (topLeftLocationAnimator != null) { locations.add(topLeftLocationAnimator.getLocation()); } if (bottomRightLocationAnimator != null) { locations.add(bottomRightLocationAnimator.getLocation()); } topLeftLocation = EquirectangularProjection.getTopLeftLocation(locations); bottomRightLocation = EquirectangularProjection.getBottomRightLocation(locations); if (edgeLocationsChanged()) { LocationListener locationListener = new LocationListener() { @Override public void onLocationUpdated(LocationProvider locationProvider, Location location) { if (!location.hasLatitudeAndLongitude()) { return; } if (locationProvider == topLeftLocationAnimator) { canvasProjection.setTopLeftLocation(location); } else if (locationProvider == bottomRightLocationAnimator) { canvasProjection.setBottomRightLocation(location); } invalidate(); } }; topLeftLocationAnimator = startLocationAnimation(topLeftLocationAnimator, topLeftLocation, locationListener); bottomRightLocationAnimator = startLocationAnimation(bottomRightLocationAnimator, bottomRightLocation, locationListener); } } private boolean edgeLocationsChanged() { if (topLeftLocationAnimator == null || bottomRightLocationAnimator == null) { return true; } boolean topLeftChanged = !topLeftLocation.latitudeAndLongitudeEquals(topLeftLocationAnimator.getTargetLocation()); boolean bottomRightChanged = !bottomRightLocation.latitudeAndLongitudeEquals(bottomRightLocationAnimator.getTargetLocation()); return topLeftChanged || bottomRightChanged; } @Override public void onLocationsChanged() { updateEdgeLocations(); super.onLocationsChanged(); } @Override public void onDeviceLocationChanged() { startDeviceRadiusAnimation(); recentLocations.add(deviceLocation); if (recentLocations.size() > 100) { recentLocations.remove(0); } super.onDeviceLocationChanged(); } public void onPredictedDeviceLocationChanged() { predictedDeviceLocationAnimator = startLocationAnimation(predictedDeviceLocationAnimator, predictedDeviceLocation, new LocationListener() { @Override public void onLocationUpdated(LocationProvider locationProvider, Location location) { invalidate(); } }); } protected void startDeviceRadiusAnimation() { if (deviceRadiusAnimator != null && deviceRadiusAnimator.isRunning()) { return; } deviceRadiusAnimator = ValueAnimator.ofFloat(0, 1); deviceRadiusAnimator.setDuration(LocationAnimator.ANIMATION_DURATION_LONG); deviceRadiusAnimator.setRepeatCount(1); deviceRadiusAnimator.setRepeatMode(ValueAnimator.REVERSE); deviceRadiusAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { invalidate(); } }); deviceRadiusAnimator.start(); } /** * Calculates a score based on the recency of the specified timestamp. * * @return score between 0 and 1 */ public static float getRecencyScore(long timestamp, long maximumAge) { long age = System.currentTimeMillis() - timestamp; long ageDelta = Math.max(0, maximumAge - age); if (ageDelta == 0 || maximumAge == 0) { return 0; } float linearScore = ageDelta / (float) maximumAge; return (float) ((Math.log(ageDelta) / Math.log(10)) / ((Math.log(maximumAge)) / Math.log(1 + (9 * linearScore)))); } /* Getter & Setter */ public Location getPredictedDeviceLocation() { return predictedDeviceLocation; } public void setPredictedDeviceLocation(Location predictedDeviceLocation) { this.predictedDeviceLocation = predictedDeviceLocation; onPredictedDeviceLocationChanged(); } public BeaconMapBackground getMapBackground() { return mapBackground; } public void setMapBackground(BeaconMapBackground mapBackground) { this.mapBackground = mapBackground; } }