app/src/main/java/ch/epfl/sdp/peakar/camera/CameraUiView.java
package ch.epfl.sdp.peakar.camera;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import androidx.core.util.Pair;
import androidx.preference.PreferenceManager;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Formatter;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import ch.epfl.sdp.peakar.R;
import ch.epfl.sdp.peakar.points.ComputePOIPoints;
import ch.epfl.sdp.peakar.points.POIPoint;
import ch.epfl.sdp.peakar.utils.CameraUtilities;
import static ch.epfl.sdp.peakar.utils.ImageHandler.rotateBitmap;
/**
* CameraUiView draws a canvas with the compass and mountain information on the camera-preview
*/
public class CameraUiView extends View implements Observer {
// computePOIPointsInstance instance
private final ComputePOIPoints computePOIPointsInstance;
//Paints used to draw the lines and heading of the compass on the camera-preview
private Paint mainLinePaint;
private Paint secondaryLinePaint;
private Paint terciaryLinePaint;
private Paint mainTextPaint;
private Paint mountainInfo;
private Paint secondaryTextPaint;
private Paint backgroundRectPaint;
//Colors of the compass-view
private int compassColor;
//Font size of the text
private int mainTextSize;
//Max opacity for the paints
private static final int MAX_ALPHA = 255;
//Rotation to be applied for the addition info of the mountains
private static final int LABEL_ROTATION = -45;
//Factors for the sizes
private static final int RADIUS_RECT_CORNER = 60;
private static final int MAIN_TEXT_FACTOR = 20;
private static final int SEC_TEXT_FACTOR = 15;
private static final int OFFSET_RECTANGLE_X_EDGE = 7;
private static final int MAIN_LINE_FACTOR = 5;
private static final int OFFSET_RECTANGLE_Y_EDGE = 4;
private static final int SEC_LINE_FACTOR = 3;
private static final int TER_LINE_FACTOR = 2;
//Heading of the user
private float horizontalDegrees;
private float verticalDegrees;
//Number of pixels per degree
private float pixDeg;
private float screenDensity;
//Range of the for-loop to draw the compass
private float minDegrees;
private float maxDegrees;
private float rangeDegreesVertical;
private float rangeDegreesHorizontal;
//Compass canvas
private Canvas canvas;
//Heights of the compass
private int textHeight;
private int mainLineHeight;
private int secondaryLineHeight;
private int terciaryLineHeight;
//Height of the view in pixel
private int height;
//Marker used to display mountains on camera-preview
private Bitmap mountainMarkerVisible;
private Bitmap mountainMarkerNotVisible;
private Bitmap distanceBitmap;
private Bitmap heightBitmap;
//Map that contains the labeled POIPoints
private Map<POIPoint, Boolean> labeledPOIPoints;
private final SharedPreferences sharedPref;
private Boolean displayedToastMode;
private Boolean displayCompass;
private static final String DISPLAY_ALL_POIS = "0";
private static final String DISPLAY_POIS_IN_SIGHT = "1";
private static final String DISPLAY_POIS_OUT_OF_SIGHT = "2";
private final List<POIPoint> discoveredPOIPoints;
@SuppressLint("SimpleDateFormat")
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd/MM/yyyy - HH:mm:ss");
private final SharedPreferences.OnSharedPreferenceChangeListener listenerPreferences =
(prefs, key) -> {
POISetter(prefs);
displayCompass = prefs.getBoolean(getResources().getString(R.string.displayCompass_key), false);
};
/**
* Constructor for the CompassView which initializes the widges like the font height and paints used
* @param context Context of the activity on which the camera-preview is drawn
* @param attrs AttributeSet so that the CompassView can be used from the xml directly
*/
public CameraUiView(Context context, AttributeSet attrs){
super(context, attrs);
widgetInit();
sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
sharedPref.registerOnSharedPreferenceChangeListener(listenerPreferences);
discoveredPOIPoints = new ArrayList<>();
displayedToastMode = false;
displayCompass = sharedPref.getBoolean(getResources().getString(R.string.displayCompass_key), false);
computePOIPointsInstance = ComputePOIPoints.getInstance(context);
computePOIPointsInstance.addObserver(this);
POISetter(sharedPref);
}
/**
* Handles the setting of the displayed POIpoints
*
* @param prefs sharedPreferences to determine the POIPoints to be displayed
*/
private void POISetter(SharedPreferences prefs) {
String displayMode = prefs.getString(getResources().getString(R.string.displayPOIs_key), DISPLAY_ALL_POIS);
boolean filterPOIs = prefs.getBoolean(getResources().getString(R.string.filterPOIs_key), true);
switch (displayMode){
case DISPLAY_ALL_POIS:
setPOIs(filterPOIs ? computePOIPointsInstance.getFilteredPOIs() : computePOIPointsInstance.getPOIs());
break;
case DISPLAY_POIS_IN_SIGHT:
setPOIs(filterPOIs ? computePOIPointsInstance.getFilteredPOIsInSight() : computePOIPointsInstance.getPOIsInSight());
checkIfLineOfSightAvailable();
break;
case DISPLAY_POIS_OUT_OF_SIGHT:
setPOIs(filterPOIs ? computePOIPointsInstance.getFilteredPOIsOutOfSight() : computePOIPointsInstance.getPOIsOutOfSight());
checkIfLineOfSightAvailable();
break;
}
}
/**
* Initializes all needed widgets for the compass like paint variables
*/
private void widgetInit(){
//Initialize colors
compassColor = getResources().getColor(R.color.Black, null);
int mountainInfoColor = getResources().getColor(R.color.White, null);
//Initialize fonts
screenDensity = getResources().getDisplayMetrics().scaledDensity;
mainTextSize = (int) (MAIN_TEXT_FACTOR * screenDensity);
//Initialize mountain marker that are in line of sight
mountainMarkerVisible = getBitmapFromVectorDrawable(getContext(), R.drawable.ic_mountain_marker_visible);
//Initialize mountain marker that are not in line of sight
mountainMarkerNotVisible = getBitmapFromVectorDrawable(getContext(), R.drawable.ic_mountain_marker_not_visible);
heightBitmap = getBitmapFromVectorDrawable(getContext(), R.drawable.ic_double_arrow);
distanceBitmap = rotateBitmap(heightBitmap);
//Initialize paints
mountainInfo = new Paint(Paint.ANTI_ALIAS_FLAG);
mountainInfo.setTextAlign(Paint.Align.LEFT);
mountainInfo.setTextSize(SEC_TEXT_FACTOR*screenDensity);
mountainInfo.setColor(mountainInfoColor);
mountainInfo.setAlpha(MAX_ALPHA);
//Paint used for the main text heading (N, E, S, W)
mainTextPaint = configureTextPaint(mainTextSize);
//Paint used for the secondary text heading (NE, SE, SW, NW)
secondaryTextPaint = configureTextPaint(SEC_TEXT_FACTOR*screenDensity);
//Paint used for the main lines (0°, 90°, 180°, ...)
mainLinePaint = configureLinePaint(MAIN_LINE_FACTOR*screenDensity);
//Paint used for the secondary lines (45°, 135°, 225°, ...)
secondaryLinePaint = configureLinePaint(SEC_LINE_FACTOR*screenDensity);
//Paint used for the terciary lines (15°, 30°, 60°, 75°, 105°, ...)
terciaryLinePaint = configureLinePaint(TER_LINE_FACTOR*screenDensity);
backgroundRectPaint = configureRectPaint();
}
public static Bitmap getBitmapFromVectorDrawable(Context context, int drawableId) {
Drawable drawable = ContextCompat.getDrawable(context, drawableId);
assert drawable != null;
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
/**
* Method to create the line paints for the compass
* @param strokeWidth width of the lines
* @return configured paint
*/
private Paint configureLinePaint(float strokeWidth){
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStrokeWidth(strokeWidth);
paint.setColor(compassColor);
paint.setAlpha(MAX_ALPHA);
return paint;
}
/**
* Method to create a rectangle paint around the text
* @return configured paint
*/
private Paint configureRectPaint(){
Paint paint = new Paint();
paint.setStrokeWidth(0);
TypedValue typedValue = new TypedValue();
Resources.Theme theme = getContext().getTheme();
theme.resolveAttribute(R.attr.colorControlNormal, typedValue, true);
paint.setColor(typedValue.data);
paint.setStyle(Paint.Style.FILL);
paint.setAlpha(100);
return paint;
}
/**
* Method to create the text paints for the compass
* @param textSize size of the text
* @return configured paint
*/
private Paint configureTextPaint(float textSize){
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(textSize);
paint.setColor(compassColor);
paint.setAlpha(MAX_ALPHA);
return paint;
}
/**
* Set the horizontal and vertical degrees for the compass and markers. When setDegrees is called,
* it updates the canvas calling invalidate() and requestLayout() which redraws the view.
* @param horizontalDegrees set the horizontal heading in degrees
* @param verticalDegrees set the vertical heading in degrees
*/
public void setDegrees(float horizontalDegrees, float verticalDegrees) {
this.horizontalDegrees = horizontalDegrees;
this.verticalDegrees = verticalDegrees;
invalidate();
requestLayout();
}
/**
* Sets the range in degrees of the compass-view, corresponds to the field of view of the camera
* @param cameraFieldOfView Pair containing the horizontal and vertical field of view
*/
@SuppressWarnings("ConstantConditions")
public void setRange(Pair<Float, Float> cameraFieldOfView) {
int orientation = getResources().getConfiguration().orientation;
//Switch horizontal and vertical fov depending on the orientation
this.rangeDegreesHorizontal = orientation==Configuration.ORIENTATION_LANDSCAPE ?
cameraFieldOfView.first : cameraFieldOfView.second;
this.rangeDegreesVertical = orientation==Configuration.ORIENTATION_LANDSCAPE ?
cameraFieldOfView.second : cameraFieldOfView.first;
}
/**
* Set the POIs that will be drawn on the camera-preview
* @param labeledPOIPoints Map of the POIPoints with the line of sight boolean
*/
public void setPOIs(Map<POIPoint, Boolean> labeledPOIPoints){
this.labeledPOIPoints = labeledPOIPoints;
invalidate();
requestLayout();
}
/**
* onDraw method is used to draw the compass on the screen.
* To draw the compass, 3 different types of lines are used, mainLinePaint, secondaryLinePaint
* and terciaryLinePaint. The compass is drawn by going through a for-loop starting from minDegree
* until maxDegrees. They correspond to the actual heading minus and plus half of the field of view
* of the device camera.
* @param canvas Canvas on which the compass is drawn
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
this.canvas = canvas;
//Get width and height of the view in Pixels
int width = getMeasuredWidth();
height = getMeasuredHeight();
//Make the canvas take 1/5 of the screen height
//The text is at the highest point
textHeight = height - height/5;
//mainLineHeight is just under the text
//The text is centered thus we add the textsize divided by 2
mainLineHeight = textHeight + mainTextSize/2;
//Then increment each by mainTextSize to get the next line height
// (the higher the result the lower the line)
secondaryLineHeight = mainLineHeight + mainTextSize;
terciaryLineHeight = secondaryLineHeight + mainTextSize;
//Get the starting degree and ending degree of the compass
minDegrees = horizontalDegrees - rangeDegreesHorizontal/2;
maxDegrees = horizontalDegrees + rangeDegreesHorizontal/2;
//Calculate the width in pixel of one degree
pixDeg = width/rangeDegreesHorizontal;
//Draws the compass
drawCanvas();
}
/**
* Draws the compass on the canvas
*/
private void drawCanvas(){
//Start going through the loop to draw the compass
for(int i = (int)Math.floor(minDegrees); i <= Math.ceil(maxDegrees); i++){
//Draw the compass
if(displayCompass){
drawCompass(i);
}
//Draw the mountains on the canvas
if(labeledPOIPoints != null && !labeledPOIPoints.isEmpty()) {
drawLabeledPOIs(i);
}
}
}
/**
* Draws the compass on the canvas. Compass consists of a for-loop going from the heading -
* fov/2 until heading + fov/2.
* @param i degree of the compass
*/
private void drawCompass(int i) {
float lineXpos = pixDeg * (i - minDegrees);
//Draw a main line and heading for every 90°
if (i % 90 == 0){
canvas.drawLine(lineXpos, height, lineXpos, mainLineHeight, mainLinePaint);
canvas.drawText(CameraUtilities.selectHeadingString(i), pixDeg * (i - minDegrees), textHeight, mainTextPaint);
}
//Draw a secondary line for every 45° excluding every 90° (45°, 135°, 225° ...)
else if (i % 45 == 0){
canvas.drawLine(lineXpos, height, lineXpos, secondaryLineHeight, secondaryLinePaint);
canvas.drawText(CameraUtilities.selectHeadingString(i), pixDeg * (i - minDegrees), textHeight, secondaryTextPaint);
}
//Draw tertiary line for every 15° excluding every 45° and 90° (15, 30, 60, 75, ...)
else if (i % 15 == 0){
canvas.drawLine(lineXpos, height, lineXpos, terciaryLineHeight, terciaryLinePaint);
}
}
/**
* Draws the POIs depending on their visibility on the display using the horizontal and
* vertical bearing of the mountain to the user
* @param actualDegree degree on which the POIPoint is drawn
*/
private void drawLabeledPOIs(int actualDegree){
//Go through all POIPoints
labeledPOIPoints.entrySet().stream()
.filter(p -> (int)p.getKey().getHorizontalBearing() == (actualDegree + 360) % 360)
.forEach(p -> drawMountainMarker(p.getKey(), p.getValue(), actualDegree));
}
/**
* Draws the mountain marker on the canvas depending on the visibility of the POIPoint and adds
* them to the discovered POIPoints if the user looks at them and they are in the line of sight
* @param poiPoint POIPoint that gets drawn
* @param isVisible Boolean that indicates if the POIPoint is visible or not
* @param actualDegree degree on which the POIPoint is drawn
*/
private void drawMountainMarker(POIPoint poiPoint, Boolean isVisible, int actualDegree){
if(isVisible && (int)poiPoint.getHorizontalBearing()== (int)horizontalDegrees && !discoveredPOIPoints.contains(poiPoint)){
Date discoveredDate = new Date();
poiPoint.setDiscoveredDate(DATE_FORMAT.format(discoveredDate));
discoveredPOIPoints.add(poiPoint);
}
//Use both results and substract the actual vertical heading
float deltaVerticalAngle = (float) (poiPoint.getVerticalBearing() - verticalDegrees);
//Calculate position in Pixel to display the mountainMarker
float mountainMarkerPosition = height * (rangeDegreesVertical - 2*deltaVerticalAngle) / (2*rangeDegreesVertical)
- (float)mountainMarkerVisible.getHeight()/2;
//Calculate the horizontal position
float left = pixDeg * (actualDegree - minDegrees);
//Draw the marker on the preview depending on the line of sight
Bitmap mountainMarker = isVisible ? mountainMarkerVisible : mountainMarkerNotVisible;
//Save status before Screen Rotation
canvas.save();
canvas.rotate(LABEL_ROTATION, left, mountainMarkerPosition);
String textName = poiPoint.getName();
float xName = left + mountainInfo.getTextSize();
float yName = mountainMarkerPosition + mountainInfo.getTextSize() + OFFSET_RECTANGLE_Y_EDGE*screenDensity;
float xBitmapHeight = xName - OFFSET_RECTANGLE_Y_EDGE*screenDensity;
float yBitmap = yName + 2*screenDensity;
String textHeight = " " + (int)poiPoint.getAltitude() + "m, ";
float xTextHeight = xBitmapHeight + heightBitmap.getWidth()/2f;
float yTextInfo = yName + mountainInfo.getTextSize();
float xBitmapDistance = xTextHeight + mountainInfo.measureText(textHeight);
Formatter mToKm = new Formatter();
mToKm.format("%.2f", poiPoint.getDistanceToUser()/1000);
String textDistance = " " + mToKm.toString() + "km";
float xTextDistance = xBitmapDistance + distanceBitmap.getWidth();
float leftRect = left - mountainMarker.getWidth()/2f;
float topRect = yName - mountainInfo.getTextSize() + 2*screenDensity;
float bottomRect = yTextInfo + OFFSET_RECTANGLE_Y_EDGE*screenDensity;
float rightRect = Math.max(xTextDistance + mountainInfo.measureText(textDistance), xName + mountainInfo.measureText(textName)) + OFFSET_RECTANGLE_X_EDGE*screenDensity;
float xNameCentered = xName + (rightRect - xName - mountainInfo.measureText(textName) - OFFSET_RECTANGLE_X_EDGE*screenDensity)/2;
//Draw first rectangle to overdraw the background
canvas.drawRoundRect(leftRect, topRect, rightRect, bottomRect, RADIUS_RECT_CORNER, RADIUS_RECT_CORNER, backgroundRectPaint);
canvas.drawText(textName, Math.max(xNameCentered, xName), yName, mountainInfo);
canvas.drawBitmap(heightBitmap, xBitmapHeight, yBitmap, null);
canvas.drawText(textHeight, xTextHeight, yTextInfo, mountainInfo);
canvas.drawBitmap(distanceBitmap, xBitmapDistance, yBitmap, null);
canvas.drawText(textDistance, xTextDistance, yTextInfo, mountainInfo);
//Restore the saved state
canvas.restore();
//Overdraw the marker on the rectangle
canvas.drawBitmap(mountainMarker, left, mountainMarkerPosition, null);
}
/**
* Gets the new discovered peaks
* @return List of the new discovered peaks
*/
public List<POIPoint> getDiscoveredPOIPoints(){
return discoveredPOIPoints;
}
/**
* Checks if the line of sight has been computed. If not display only one toast informing the user
*/
private void checkIfLineOfSightAvailable() {
if(!computePOIPointsInstance.isLineOfSightAvailable() && !displayedToastMode){
Toast.makeText(getContext(), getResources().getString(R.string.lineOfSightNotDownloaded), Toast.LENGTH_SHORT).show();
displayedToastMode = true;
}
}
/**
* Get a bitmap of the compass-view
* @return a bitmap of the compass-view
*/
public Bitmap getBitmap(){
CameraUiView cameraUiView = findViewById(R.id.compass);
cameraUiView.setDrawingCacheEnabled(true);
cameraUiView.buildDrawingCache();
Bitmap bitmap = Bitmap.createBitmap(cameraUiView.getDrawingCache());
cameraUiView.setDrawingCacheEnabled(false);
return bitmap;
}
@Override
public void update(Observable o, Object arg) {
POISetter(sharedPref);
}
}