1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.car.cluster.view;
17 
18 import android.annotation.Nullable;
19 import android.car.cluster.navigation.NavigationState.ImageReference;
20 import android.car.cluster.navigation.NavigationState.Lane;
21 import android.car.cluster.navigation.NavigationState.Lane.LaneDirection;
22 import android.content.Context;
23 import android.graphics.Bitmap;
24 import android.graphics.Canvas;
25 import android.graphics.PorterDuff;
26 import android.graphics.PorterDuffColorFilter;
27 import android.graphics.drawable.Drawable;
28 import android.graphics.drawable.VectorDrawable;
29 import android.os.Handler;
30 import android.util.AttributeSet;
31 import android.util.Log;
32 import android.widget.ImageView;
33 import android.widget.LinearLayout;
34 
35 import com.android.car.cluster.osdouble.R;
36 
37 import java.util.ArrayList;
38 import java.util.List;
39 
40 /**
41  * View component that displays the Lane preview information on the instrument cluster display
42  */
43 public class LaneView extends LinearLayout {
44     private static final String TAG = "Cluster.LaneView";
45 
46     private Handler mHandler = new Handler();
47 
48     private ArrayList<Lane> mLanes;
49 
50     private final int mWidth = (int) getResources().getDimension(R.dimen.lane_width);
51     private final int mHeight = (int) getResources().getDimension(R.dimen.lane_height);
52     private final int mOffset = (int) getResources().getDimension(R.dimen.lane_icon_offset);
53 
54     private enum Shift {
55         LEFT,
56         RIGHT,
57         BOTH
58     }
59 
LaneView(Context context)60     public LaneView(Context context) {
61         super(context);
62     }
63 
LaneView(Context context, AttributeSet attrs)64     public LaneView(Context context, AttributeSet attrs) {
65         super(context, attrs);
66     }
67 
LaneView(Context context, AttributeSet attrs, int defStyleAttr)68     public LaneView(Context context, AttributeSet attrs, int defStyleAttr) {
69         super(context, attrs, defStyleAttr);
70     }
71 
setLanes(ImageReference imageReference, ImageResolver imageResolver)72     public void setLanes(ImageReference imageReference, ImageResolver imageResolver) {
73         imageResolver
74                 .getBitmap(imageReference, 0, getHeight(), 0.5f)
75                 .thenAccept(bitmap -> {
76                     mHandler.post(() -> {
77                         removeAllViews();
78                         ImageView imgView = new ImageView(getContext());
79                         imgView.setImageBitmap(bitmap);
80                         imgView.setAdjustViewBounds(true);
81                         addView(imgView);
82                     });
83                 })
84                 .exceptionally(ex -> {
85                     removeAllViews();
86                     if (Log.isLoggable(TAG, Log.DEBUG)) {
87                         Log.d(TAG, "Unable to fetch image for lane: " + imageReference);
88                     }
89                     return null;
90                 });
91     }
92 
setLanes(List<Lane> lanes, float alpha)93     public void setLanes(List<Lane> lanes, float alpha) {
94         mLanes = new ArrayList<>(lanes);
95         removeAllViews();
96 
97         // Use drawables for lane directional guidance
98         for (Lane lane : mLanes) {
99             Bitmap bitmap = combineBitmapFromLane(lane);
100             ImageView imgView = new ImageView(getContext());
101             imgView.setImageBitmap(bitmap);
102             imgView.setAdjustViewBounds(true);
103             imgView.setImageAlpha((int) (alpha * 255));
104             addView(imgView);
105         }
106     }
107 
combineBitmapFromLane(Lane lane)108     private Bitmap combineBitmapFromLane(Lane lane) {
109         if (lane.getLaneDirectionsList().isEmpty()) {
110             return null;
111         }
112 
113         Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
114         Canvas canvas = new Canvas(bitmap);
115 
116         Shift shift = getShift(lane);
117 
118         for (LaneDirection laneDir : lane.getLaneDirectionsList()) {
119             if (!laneDir.getIsHighlighted()) {
120                 drawToCanvas(laneDir, canvas, false, shift);
121             }
122         }
123 
124         for (LaneDirection laneDir : lane.getLaneDirectionsList()) {
125             if (laneDir.getIsHighlighted()) {
126                 drawToCanvas(laneDir, canvas, true, shift);
127             }
128         }
129 
130         return bitmap;
131     }
132 
drawToCanvas(LaneDirection laneDir, Canvas canvas, boolean isHighlighted, Shift shift)133     private void drawToCanvas(LaneDirection laneDir, Canvas canvas, boolean isHighlighted,
134             Shift shift) {
135         int offset = getOffset(laneDir, shift);
136         VectorDrawable icon = (VectorDrawable) getLaneIcon(laneDir);
137         icon.setBounds(offset, 0, mWidth + offset, mHeight);
138         icon.setColorFilter(new PorterDuffColorFilter(isHighlighted
139                 ? getContext().getColor(R.color.laneDirectionHighlighted)
140                 : getContext().getColor(R.color.laneDirection),
141                 PorterDuff.Mode.SRC_ATOP));
142         icon.draw(canvas);
143     }
144 
145     /**
146      * Determines the offset direction to line up overlapping lane directions.
147      */
getShift(Lane lane)148     private Shift getShift(Lane lane) {
149         boolean containsRight = false;
150         boolean containsLeft = false;
151         boolean containsStraight = false;
152 
153         for (LaneDirection laneDir : lane.getLaneDirectionsList()) {
154             if (laneDir.getShape().equals(LaneDirection.Shape.NORMAL_RIGHT)
155                     || laneDir.getShape().equals(LaneDirection.Shape.SLIGHT_RIGHT)
156                     || laneDir.getShape().equals(LaneDirection.Shape.SHARP_RIGHT)
157                     || laneDir.getShape().equals(LaneDirection.Shape.U_TURN_RIGHT)) {
158                 containsRight = true;
159             }
160             if (laneDir.getShape().equals(LaneDirection.Shape.NORMAL_LEFT)
161                     || laneDir.getShape().equals(LaneDirection.Shape.SLIGHT_LEFT)
162                     || laneDir.getShape().equals(LaneDirection.Shape.SHARP_LEFT)
163                     || laneDir.getShape().equals(LaneDirection.Shape.U_TURN_LEFT)) {
164                 containsLeft = true;
165             }
166             if (laneDir.getShape().equals(LaneDirection.Shape.STRAIGHT)) {
167                 containsStraight = true;
168             }
169         }
170 
171         if (containsLeft && containsRight) {
172             //shift turns outwards
173             return Shift.BOTH;
174         } else if (containsStraight && containsRight) {
175             //shift straight lane dir to the left
176             return Shift.LEFT;
177         } else if (containsStraight && containsLeft) {
178             //shift straight lane dir to the right
179             return Shift.RIGHT;
180         }
181 
182         return null;
183     }
184 
185     /**
186      * Returns the offset value of the lane direction based on the given shift direction.
187      */
getOffset(LaneDirection laneDir, Shift shift)188     private int getOffset(LaneDirection laneDir, Shift shift) {
189         if (shift == Shift.BOTH) {
190             if (laneDir.getShape().equals(LaneDirection.Shape.NORMAL_LEFT)
191                     || laneDir.getShape().equals(LaneDirection.Shape.SLIGHT_LEFT)
192                     || laneDir.getShape().equals(LaneDirection.Shape.SHARP_LEFT)
193                     || laneDir.getShape().equals(LaneDirection.Shape.U_TURN_LEFT)) {
194                 return -mOffset;
195             }
196             if (laneDir.getShape().equals(LaneDirection.Shape.NORMAL_RIGHT)
197                     || laneDir.getShape().equals(LaneDirection.Shape.SLIGHT_RIGHT)
198                     || laneDir.getShape().equals(LaneDirection.Shape.SHARP_RIGHT)
199                     || laneDir.getShape().equals(LaneDirection.Shape.U_TURN_RIGHT)) {
200                 return mOffset;
201             }
202         } else if (shift == Shift.LEFT) {
203             if (laneDir.getShape().equals(LaneDirection.Shape.STRAIGHT)) {
204                 return -mOffset;
205             }
206         } else if (shift == Shift.RIGHT) {
207             if (laneDir.getShape().equals(LaneDirection.Shape.STRAIGHT)) {
208                 return mOffset;
209             }
210         }
211 
212         return 0;
213     }
214 
getLaneIcon(@ullable LaneDirection laneDir)215     private Drawable getLaneIcon(@Nullable LaneDirection laneDir) {
216         if (laneDir == null) {
217             return null;
218         }
219         switch (laneDir.getShape()) {
220             case UNKNOWN:
221                 return null;
222             case STRAIGHT:
223                 return mContext.getDrawable(R.drawable.direction_continue);
224             case SLIGHT_LEFT:
225                 return mContext.getDrawable(R.drawable.direction_turn_slight_left);
226             case SLIGHT_RIGHT:
227                 return mContext.getDrawable(R.drawable.direction_turn_slight_right);
228             case NORMAL_LEFT:
229                 return mContext.getDrawable(R.drawable.direction_turn_left);
230             case NORMAL_RIGHT:
231                 return mContext.getDrawable(R.drawable.direction_turn_right);
232             case SHARP_LEFT:
233                 return mContext.getDrawable(R.drawable.direction_turn_sharp_left);
234             case SHARP_RIGHT:
235                 return mContext.getDrawable(R.drawable.direction_turn_sharp_right);
236             case U_TURN_LEFT:
237                 return mContext.getDrawable(R.drawable.direction_uturn_left);
238             case U_TURN_RIGHT:
239                 return mContext.getDrawable(R.drawable.direction_uturn_right);
240         }
241         return null;
242     }
243 }
244