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